Версия для печати

Архив документации на OpenNet.ru / Раздел "Программирование, языки" (Многостраничная версия)

Advanced Bash-Scripting Guide

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

Автор: Mendel Cooper

Перевод: Андрей Киселев


Оригинал: Russian Linux Gazette
Архив руководства в html-формате (~380Кб)
Архив руководства в sgml-формате (~380Кб)

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

Последнюю версию документа, в виде .bz2 архива, содержащем исходные тексты в формате SGML и HTML, вы найдете на домашней страничке автора. Там же вы найдете и change log.


Посвящения

Посвящается Аните -- источнику очарования

Содержание
Часть 1. Введение
1. Зачем необходимо знание языка Shell?
2. Для начала о Sha-Bang
2.1. Запуск сценария
2.2. Упражнения
Часть 2. Основы
3. Служебные символы
4. Переменные и параметры. Введение.
4.1. Подстановка переменных
4.2. Присваивание значений переменным
4.3. Переменные Bash не имеют типа
4.4. Специальные типы переменных
5. Кавычки
6. Завершение и код завершения
7. Проверка условий
7.1. Конструкции проверки условий
7.2. Операции проверки файлов
7.3. Операции сравнения
7.4. Вложенные условные операторы if/then
7.5. Проверка степени усвоения материала
8. Операции и смежные темы
8.1. Операторы
8.2. Числовые константы
Часть 3. Углубленный материал
9. К вопросу о переменных
9.1. Внутренние переменные
9.2. Работа со строками
9.3. Подстановка параметров
9.4. Объявление переменных: declare и typeset
9.5. Косвенные ссылки на переменные
9.6. $RANDOM: генерация псевдослучайных целых чисел
9.7. Двойные круглые скобки
10. Циклы и ветвления
10.1. Циклы
10.2. Вложенные циклы
10.3. Управление ходом выполнения цикла
10.4. Операторы выбора
11. Внутренние команды
11.1. Команды управления заданиями
12. Внешние команды, программы и утилиты
12.1. Базовые команды
12.2. Более сложные команды
12.3. Команды для работы с датой и временем
12.4. Команды обработки текста
12.5. Команды для работы с файлами и архивами
12.6. Команды для работы с сетью
12.7. Команды управления терминалом
12.8. Команды выполнения математических операций
12.9. Прочие команды
13. Команды системного администрирования
14. Подстановка команд
15. Арифметические подстановки
16. Перенаправление ввода/вывода
16.1. С помощью команды exec
16.2. Перенаправление для блоков кода
16.3. Область применения
17. Встроенные документы
Часть 4. Материал повышенной сложности
18. Регулярные выражения
18.1. Краткое введение в регулярные выражения
18.2. Globbing -- Подстановка имен файлов
19. Подоболочки, или Subshells
20. Ограниченный режим командной оболочки
21. Подстановка процессов
22. Функции
22.1. Сложные функции и сложности с функциями
22.2. Локальные переменные
23. Псевдонимы
24. Списки команд
25. Массивы
26. Файлы
27. /dev и /proc
27.1. /dev
27.2. /proc
28. /dev/zero и /dev/null
29. Отладка сценариев
30. Необязательные параметры (ключи)
31. Широко распространенные ошибки
32. Стиль программирования
32.1. Неофициальные рекомендации по оформлению сценариев
33. Разное
33.1. Интерактивный и неинтерактивный режим работы
33.2. Сценарии-обертки
33.3. Операции сравнения: Альтернативные решения
33.4. Рекурсия
33.5. "Цветные" сценарии
33.6. Оптимизация
33.7. Разные советы
33.8. Проблемы безопасности
33.9. Проблемы переносимости
33.10. Сценарии командной оболочки под Windows
34. Bash, версия 2
35. Замечания и дополнения
35.1. От автора
35.2. Об авторе
35.3. Инструменты, использовавшиеся при создании книги
35.3.1. Аппаратура
35.3.2. Программное обеспечение
35.4. Благодарности
Литература
A. Дополнительные примеры сценариев
B. Маленький учебник по Sed и Awk
B.1. Sed
B.2. Awk
C. Коды завершения, имеющие предопределенный смысл
D. Подробное введение в операции ввода-вывода и перенаправление ввода-вывода
E. Локализация
F. История команд
G. Пример файла .bashrc
H. Преобразование пакетных (*.bat) файлов DOS в сценарии командной оболочки
I. Упражнения
I.1. Анализ сценариев
I.2. Создание сценариев
J. Авторские права
Перечень таблиц
11-1. Идентификация заданий
30-1. Ключи Bash
33-1. Числовые значения цвета в escape-последовательностях
B-1. Основные операции sed
B-2. Примеры операций в sed
C-1. "Зарезервированные" коды завершения
H-1. Ключевые слова/переменные/операторы пакетных файлов DOS и их аналоги командной оболочки
H-2. Команды DOS и их эквиваленты в UNIX
Перечень приложений
2-1. cleanup: Сценарий очистки лог-файлов в /var/log
2-2. cleanup: Расширенная версия предыдущего сценария.
3-1. Вложенные блоки и перенаправление ввода-вывода
3-2. Сохранение результата исполнения вложенного блока в файл
3-3. Запуск цикла в фоновом режиме
3-4. Резервное архивирование всех файлов, которые были изменены в течение последних суток
4-1. Присваивание значений переменным и подстановка значений переменных
4-2. Простое присваивание
4-3. Присваивание значений переменным простое и замаскированное
4-4. Целое число или строка?
4-5. Позиционные параметры
4-6. wh, whois выяснение имени домена
4-7. Использование команды shift
5-1. Вывод "причудливых" переменных
5-2. Экранированные символы
6-1. завершение / код завершения
6-2. Использование символа ! для логической инверсии кода возврата
7-1. Что есть "истина"?
7-2. Эквиваленты команды test -- /usr/bin/test, [ ], и /usr/bin/[
7-3. Арифметические выражения внутри (( ))
7-4. Проверка "битых" ссылок
7-5. Операции сравнения
7-6. Проверка -- является ли строка пустой
7-7. zmost
8-1. Наибольший общий делитель
8-2. Арифметические операции
8-3. Построение сложных условий, использующих && и ||
8-4. Различные представления числовых констант
9-1. $IFS и пробельные символы
9-2. Ограничения времени ожидания ввода
9-3. Еще один пример ограничения времени ожидания ввода от пользователя
9-4. Ограничение времени ожидания команды read
9-5. Я -- root?
9-6. arglist: Вывод списка аргументов с помощью переменных $* и $@
9-7. Противоречия в переменных $* и $@
9-8. Содержимое $* и $@, когда переменная $IFS -- пуста
9-9. Переменная "подчеркивание"
9-10. Вставка пустых строк между параграфами в текстовом файле
9-11. Преобразование графических файлов из одного формата в другой, с изменением имени файла
9-12. Альтернативный способ извлечения подстрок
9-13. Подстановка параметров и сообщения об ошибках
9-14. Подстановка параметров и сообщение о "порядке использования"
9-15. Длина переменной
9-16. Поиск по шаблону в подстановке параметров
9-17. Изменение расширений в именах файлов:
9-18. Поиск по шаблону при анализе произвольных строк
9-19. Поиск префиксов и суффиксов с заменой по шаблону
9-20. Объявление переменных с помощью инструкции declare
9-21. Косвенные ссылки
9-22. Передача косвенных ссылок в awk
9-23. Генерация случайных чисел
9-24. Выбор случайной карты из колоды
9-25. Имитация бросания кубика с помощью RANDOM
9-26. Переустановка RANDOM
9-27. Получение псевдослучайных чисел с помощью awk
9-28. Работа с переменными в стиле языка C
10-1. Простой цикл for
10-2. Цикл for с двумя параметрами в каждом из элементов списка
10-3. Fileinfo: обработка списка файлов, находящегося в переменной
10-4. Обработка списка файлов в цикле for
10-5. Цикл for без списка аргументов
10-6. Создание списка аргументов в цикле for с помощью операции подстановки команд
10-7. grep для бинарных файлов
10-8. Список всех пользователей системы
10-9. Проверка авторства всех бинарных файлов в текущем каталоге
10-10. Список символических ссылок в каталоге
10-11. Список символических ссылок в каталоге, сохраняемый в файле
10-12. C-подобный синтаксис оператора цикла for
10-13. Работа с командой efax в пакетном режиме
10-14. Простой цикл while
10-15. Другой пример цикла while
10-16. Цикл while с несколькими условиями
10-17. C-подобный синтаксис оформления цикла while
10-18. Цикл until
10-19. Вложенный цикл
10-20. Команды break и continue в цикле
10-21. Прерывание многоуровневых циклов
10-22. Передача управление в начало внешнего цикла
10-23. Живой пример использования "continue N"
10-24. Использование case
10-25. Создание меню с помощью case
10-26. Оператор case допускает использовать подстановку команд вместо анализируемой переменной
10-27. Простой пример сравнения строк
10-28. Проверка ввода
10-29. Создание меню с помощью select
10-30. Создание меню с помощью select в функции
11-1. printf в действии
11-2. Ввод значений переменных с помощью read
11-3. Пример использования команды read без указания переменной для ввода
11-4. Ввод многострочного текста с помощью read
11-5. Обнаружение нажатия на курсорные клавиши
11-6. Чтение командой read из файла через перенаправление
11-7. Смена текущего каталога
11-8. Команда let, арифметические операции.
11-9. Демонстрация команды eval
11-10. Принудительное завершение сеанса
11-11. Шифрование по алгоритму "rot13"
11-12. Замена имени переменной на ее значение, в исходном тексте программы на языке Perl, с помощью eval
11-13. Установка значений аргументов с помощью команды set
11-14. Изменение значений позиционных параметров (аргументов)
11-15. "Сброс" переменной
11-16. Передача переменных во вложенный сценарий awk, с помощью export
11-17. Прием опций/аргументов, передаваемых сценарию, с помощью getopts
11-18. "Подключение" внешнего файла
11-19. Пример (бесполезный) сценария, который подключает себя самого.
11-20. Команда exec
11-21. Сценарий, который запускает себя самого
11-22. Ожидание завершения процесса перед тем как продолжить работу
11-23. Сценарий, завершающий себя сам с помощью команды kill
12-1. Создание оглавления диска для записи CDR, с помощью команды ls
12-2. Badname, удаление файлов в текущем каталоге, имена которых содержат недопустимые символы и пробелы.
12-3. Удаление файла по его номеру inode
12-4. Использование команды xargs для мониторинга системного журнала
12-5. copydir, копирование файлов из текущего каталога в другое место, с помощью xargs
12-6. Пример работы с expr
12-7. Команда date
12-8. Частота встречаемости отдельных слов
12-9. Какие из файлов являются сценариями?
12-10. Генератор 10-значных случайных чисел
12-11. Мониторинг системного журнала с помощью tail
12-12. Сценарий-эмулятор "grep"
12-13. Поиск слов в словаре
12-14. toupper: Преобразование символов в верхний регистр.
12-15. lowercase: Изменение имен всех файлов в текущем каталоге в нижний регистр.
12-16. du: Преобразование текстового файла из формата DOS в формат UNIX.
12-17. rot13: Сверхслабое шифрование по алгоритму rot13.
12-18. Более "сложный" шифр
12-19. Отформатированный список файлов.
12-20. Пример форматирования списка файлов в каталоге
12-21. nl: Самонумерующийся сценарий.
12-22. Пример перемещения дерева каталогов с помощью cpio
12-23. Распаковка архива rpm
12-24. Удаление комментариев из файла с текстом программы на языке C
12-25. Исследование каталога /usr/X11R6/bin
12-26. "Расширенная" команда strings
12-27. Пример сравнения двух файлов с помощью cmp.
12-28. Утилиты basename и dirname
12-29. Проверка целостности файла
12-30. Декодирование файлов
12-31. Сценарий, отправляющий себя самого по электронной почте
12-32. Ежемесячные выплаты по займу
12-33. Перевод чисел из одной системы счисления в другую
12-34. Пример взаимодействия bc со "встроенным документом"
12-35. Вычисление числа "пи"
12-36. Преобразование чисел из десятичной в шестнадцатиричную систему счисления
12-37. Разложение числа на простые множители
12-38. Расчет гипотенузы прямоугольного треугольника
12-39. Использование seq для генерации списка аргументов цикла for
12-40. Использование getopt для разбора аргументов командной строки
12-41. Захват нажатых клавиш
12-42. Надежное удаление файла
12-43. Генератор имен файлов
12-44. Преобразование метров в мили
12-45. Пример работы с m4
13-1. Установка символа "забоя"
13-2. невидимый пароль: Отключение эхо-вывода на терминал
13-3.
13-4. Использование команды pidof при остановке процесса
13-5. Проверка образа CD
13-6. Создание файловой системы в обычном файле
13-7. Добавление нового жесткого диска
13-8. Сценарий killall, из каталога /etc/rc.d/init.d
14-1. Глупая выходка
14-2. Запись результатов выполнения цикла в переменную
16-1. Перенаправление stdin с помощью exec
16-2. Перенаправление stdout с помощью exec
16-3. Одновременное перенаправление устройств, stdin и stdout, с помощью команды exec
16-4. Перенаправление в цикл while
16-5. Альтернативная форма перенаправления в цикле while
16-6. Перенаправление в цикл until
16-7. Перенаправление в цикл for
16-8. Перенаправление устройств (stdin и stdout) в цикле for
16-9. Перенаправление в конструкции if/then
16-10. Файл с именами "names.data", для примеров выше
16-11. Регистрация событий
17-1. dummyfile: Создание 2-х строчного файла-заготовки
17-2. broadcast: Передача сообщения всем, работающим в системе, пользователям
17-3. Вывод многострочных сообщений с помощью cat
17-4. Вывод многострочных сообщений с подавлением символов табуляции
17-5. Встроенные документы и подстановка параметров
17-6. Отключение подстановки параметров
17-7. Передача пары файлов во входящий каталог на "Sunsite"
17-8. Встроенные документы и функции
17-9. "Анонимный" Встроенный Документ
17-10. Блочный комментарий
17-11. Встроенная справка к сценарию
19-1. Область видимости переменных
19-2. Личные настройки пользователей
19-3. Запуск нескольких процессов в подоболочках
20-1. Запуск сценария в ограниченном режиме
22-1. Простая функция
22-2. Функция с аргументами
22-3. Наибольшее из двух чисел
22-4. Преобразование чисел в римскую форму записи
22-5. Проверка возможности возврата функциями больших значений
22-6. Сравнение двух больших целых чисел
22-7. Настоящее имя пользователя
22-8. Область видимости локальных переменных
22-9. Использование локальных переменных при рекурсии
23-1. Псевдонимы в сценарии
23-2. unalias: Объявление и удаление псевдонимов
24-1. Проверка аргументов командной строки с помощью "И-списка"
24-2. Еще один пример проверки аргументов с помощью "И-списков"
24-3. Комбинирование "ИЛИ-списков" и "И-списков"
25-1. Простой массив
25-2. Форматирование стихотворения
25-3. Некоторые специфичные особенности массивов
25-4. Пустые массивы и пустые элементы
25-5. Копирование и конкатенация массивов
25-6. Старая, добрая: "Пузырьковая" сортировка
25-7. Вложенные массивы и косвенные ссылки
25-8. Пример реализации алгоритма Решето Эратосфена
25-9. Эмуляция структуры "СТЕК" ("первый вошел -- последний вышел")
25-10. Исследование математических последовательностей
25-11. Эмуляция массива с двумя измерениями
27-1. Поиск файла программы по идентификатору процесса
27-2. Проверка состояния соединения
28-1. Удаление cookie-файлов
28-2. Создание файла подкачки (swapfile), с помощью /dev/zero
28-3. Создание электронного диска
29-1. Сценарий, содержащий ошибку
29-2. Пропущено ключевое слово
29-3. test24
29-4. Проверка условия с помощью функции "assert"
29-5. Ловушка на выходе
29-6. Удаление временного файла при нажатии на Control-C
29-7. Трассировка переменной
31-1. Западня в подоболочке
31-2. Передача вывода от команды echo команде read, по конвейеру
33-1. сценарий-обертка
33-2. Более сложный пример сценария-обертки
33-3. Сценарий-обертка вокруг сценария awk
33-4. Сценарий на языке Perl, встроенный в Bash-скрипт
33-5. Комбинирование сценария Bash и Perl в одном файле
33-6. Сценарий (бесполезный), который вызывает себя сам
33-7. Сценарий имеющий практическую ценность), который вызывает себя сам
33-8. "Цветная" адресная книга
33-9. Вывод цветного текста
33-10. Необычный способ передачи возвращаемого значения
33-11. Необычный способ получения нескольких возвращаемых значений
33-12. Передача массива в функцию и возврат массива из функции
33-13. Игры с анаграммами
34-1. Расширение строк
34-2. Косвенные ссылки на переменные -- новый метод
34-3. Простая база данных, с применением косвенных ссылок
34-4. Массивы и другие хитрости для раздачи колоды карт в четыре руки
A-1. manview: Просмотр страниц руководств man
A-2. mailformat: Форматирование электронных писем
A-3. rn: Очень простая утилита для переименования файлов
A-4. blank-rename: переименование файлов, чьи имена содержат пробелы
A-5. encryptedpw: Передача файла на ftp-сервер, с использованием пароля
A-6. copy-cd: Копирование компакт-дисков с данными
A-7. Последовательности Коллаца (Collatz)
A-8. days-between: Подсчет числа дней между двумя датами
A-9. Создание "словаря"
A-10. Расчет индекса "созвучности"
A-11. "Игра "Жизнь""
A-12. Файл с первым поколением для игры "Жизнь"
A-13. behead: Удаление заголовков из электронных писем и новостей
A-14. ftpget: Скачивание файлов по ftp
A-15. Указание на авторские права
A-16. password: Генератор случайного 8-ми символьного пароля
A-17. fifo: Создание резервных копий с помощью именованных каналов
A-18. Генерация простых чисел, с использованием оператора деления по модулю (остаток от деления)
A-19. tree: Вывод дерева каталогов
A-20. Функции для работы со строками
A-21. Directory information
A-22. Объектно ориентированная база данных
G-1. Пример файла .bashrc
H-1. VIEWDATA.BAT: пакетный файл DOS
H-2. viewdata.sh: Результат преобразования VIEWDATA.BAT в сценарий командной оболочки

Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Приложение A. Дополнительные примеры сценариев

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

Пример 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-15.

#! /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

Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Приложение B. Маленький учебник по Sed и Awk

В этом приложении содержится очень краткое описание приемов работы с утилитами обработки текста sed и awk. Здесь будут рассмотрены лишь несколько базовых команд, которых, в принципе, будет достаточно, чтобы научиться понимать простейшие конструкции sed и awk внутри сценариев на языке командной оболочки.

sed: неинтерактивный редактор текстовых файлов

awk: язык обработки шаблонов с C-подобным синтаксисом

При всех своих различиях, эти две утилиты обладают похожим синтаксисом, они обе умеют работать с регулярными выражениями, обе, по-умолчанию, читают данные с устройства stdin и обе выводят результат обработки на устройство stdout. Обе являются утилитами UNIX-систем, и прекрасно могут взаимодействовать между собой. Вывод от одной может быть перенаправлен, по конвейеру, на вход другой. Их комбинирование придает сценариям, на языке командной оболочки, мощь и гибкость языка Perl.

Note

Одно важное отличие состоит в том, что в случае с sed, сценарий легко может передавать дополнительные аргументы этой утилите, в то время, как в случае с awk (см. Пример 33-3 и Пример 9-22), это более сложная задача .

B.1. Sed

Sed -- это неинтерактивный строчный редактор. Он принимает текст либо с устройства stdin, либо из текстового файла, выполняет некоторые операции над строками и затем выводит результат на устройство stdout или в файл. Как правило, в сценариях, sed используется в конвейерной обработке данных, совместно с другими командами и утилитами.

Sed определяет, по заданному адресному пространству, над какими строками следует выполнить операции. [1] Адресное пространство строк задается либо их порядковыми номерами, либо шаблоном. Например, команда 3d заставит sed удалить третью строку, а команда /windows/d означает, что все строки, содержащие "windows", должны быть удалены.

Из всего разнообразия операций, мы остановимся на трех, используемых наиболее часто. Это p -- печать (на stdout), d -- удаление и s -- замена.

Таблица B-1. Основные операции sed

Операция Название Описание
[диапазон строк]/p print Печать [указанного диапазона строк]
[диапазон строк]/d delete Удалить [указанный диапазон строк]
s/pattern1/pattern2/ substitute Заменить первое встреченное соответствие шаблону pattern1, в строке, на pattern2
[диапазон строк]/s/pattern1/pattern2/ substitute Заменить первое встреченное соответствие шаблону pattern1, на pattern2, в указанном диапазоне строк
[диапазон строк]/y/pattern1/pattern2/ transform заменить любые символы из шаблона pattern1 на соответствующие символы из pattern2, в указанном диапазоне строк (эквивалент команды tr)
g global Операция выполняется над всеми найденными соответствиями внутри каждой из заданных строк
Note

Без оператора g (global), операция замены будет производиться только для первого найденного совпадения, с заданным шаблоном, в каждой строке.

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

sed -e '/^$/d' $filename
#  Ключ -e говорит о том, что далее следует строка, которая должна интерпретироваться
#+ как набор инструкций редактирования.
# (При передаче одной инструкции, ключ "-e" является необязательным.)
#  "Строгие" кавычки ('') предотвращают интерпретацию символов регулярного выражения,
#+ как специальных символов, командным интерпретатором.
#
# Действия производятся над строками, содержащимися в файле $filename.


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

filename=file1.txt
pattern=BEGIN

  sed "/^$pattern/d" "$filename"  # Результат вполне предсказуем.
# sed '/^$pattern/d' "$filename"    дает иной результат.
#        В данном случае, в "строгих" кавычках (' ... '),
#+      не происходит подстановки значения переменной "$pattern".


Note

Sed использует ключ -e для того, чтобы определить, что следующая строка является инструкцией, или набором инструкций, редактирования. Если инструкция является единственной, то использование этого ключа не является обязательным.

sed -n '/xzy/p' $filename
# Ключ -n заставляет sed вывести только те строки, которые совпадают с указанным шаблоном.
# В противном случае (без ключа -n), будут выведены все строки.
# Здесь, ключ -e не является обязательным, поскольку здесь стоит единственная команда.


Таблица B-2. Примеры операций в sed

Операция Описание
8d Удалить 8-ю строку.
/^$/d Удалить все пустые строки.
1,/^$/d Удалить все строки до первой пустой строки, включительно.
/Jones/p Вывести строки, содержащие "Jones" (с ключом -n).
s/Windows/Linux/ В каждой строке, заменить первое встретившееся слово "Windows" на слово "Linux".
s/BSOD/stability/g В каждой строке, заменить все встретившиеся слова "BSOD" на "stability".
s/ *$// Удалить все пробелы в конце каждой строки.
s/00*/0/g Заменить все последовательности ведущих нулей одним символом "0".
/GUI/d Удалить все строки, содержащие "GUI".
s/GUI//g Удалить все найденные "GUI", оставляя остальную часть строки без изменений.

Замена строки пустой строкой, эквивалентна удалению части строки, совпадающей с шаблоном. Остальная часть строки остается без изменений. Например, s/GUI//, изменит следующую строку

The most important parts of any application are its GUI and sound effects
на
The most important parts of any application are its  and sound effects


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

s/^  */\
/g
Эта инструкция заменит начальные пробелы в строке на символ перевода строки. Ожидаемый результат -- замена отступов в начале параграфа пустыми строками.

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

/[0-9A-Za-z]/,/^$/{
/^$/d
}
В этом случае будут удалены только первые из нескольких, идущих подряд, пустых строк. Это может использоваться для установки однострочных интервалов в файле, оставляя, при этом, пустые строки между параграфами.

Tip

Быстрый способ установки двойных межстрочных интервалов в текстовых файлах -- sed G filename.

Примеры использования sed в сценариях командной оболочки, вы найдете в:

  1. Пример 33-1

  2. Пример 33-2

  3. Пример 12-2

  4. Пример A-3

  5. Пример 12-12

  6. Пример 12-20

  7. Пример A-13

  8. Пример A-19

  9. Пример 12-24

  10. Пример 10-9

  11. Пример 12-33

  12. Пример A-2

  13. Пример 12-10

  14. Пример 12-8

  15. Пример A-11

  16. Пример 17-11



Ссылки на дополнительные сведения о sed, вы найдете в разделе Литература.

Примечания

[1]

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


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Приложение C. Коды завершения, имеющие предопределенный смысл

Таблица C-1. "Зарезервированные" коды завершения

Код завершения Смысл Пример Примечание
1 разнообразные ошибки let "var1 = 1/0" различные ошибки, такие как "деление на ноль" и пр.
2 согласно документации к Bash -- неверное использование встроенных команд   Встречаются довольно редко, обычно код завершения возвращается равным 1
126 вызываемая команда не может быть выполнена   возникает из-за проблем с правами доступа или когда вызван на исполнение неисполняемый файл
127 "команда не найдена"   Проблема связана либо с переменной окружения $PATH, либо с неверным написанием имени команды
128 неверный аргумент команды exit exit 3.14159 команда exit может принимать только целочисленные значения, в диапазоне 0 - 255
128+n фатальная ошибка по сигналу "n" kill -9 $PPID сценария $? вернет 137 (128 + 9)
130 завершение по Control-C   Control-C -- это выход по сигналу 2, (130 = 128 + 2, см. выше)
255* код завершения вне допустимого диапазона exit -1 exit может принимать только целочисленные значения, в диапазоне 0 - 255

Согласно этой таблице, коды завершения 1 - 2, 126 - 165 и 255 [1] имеют предопределенное значение, поэтому вам следует избегать употребления этих кодов для своих нужд. Завершение сценария с кодом возврата exit 127, может привести в замешательство при поиске ошибок в сценарии (действительно ли он означает ошибку "команда не найдена"? Или это предусмотренный программистом код завершения?). В большинстве случаев, программисты вставляют exit 1, в качестве реакции на ошибку. Так как код завершения 1 подразумевает целый "букет" ошибок, то в данном случае трудно говорить о какой либо двусмысленности, хотя и об информативности -- тоже.

Не раз предпринимались попытки систематизировать коды завершения (см. /usr/include/sysexits.h), но эта систематизация предназначена для программистов, пишущих на языках C и C++. Автор документа предлагает ограничить коды завершения, определяемые пользователем, диапазоном 64 - 113 (и, само собой разумеется -- 0, для обозначения успешного завершения), в соответствии со стандартом C/C++. Это сделало бы поиск ошибок более простым.

Все сценарии, прилагаемые к данному документу, приведены в соответствие с этим стандартом, за исключением случаев, когда существуют отменяющие обстоятельства, например в Пример 9-2.

Note

Обращение к переменной $?, из командной строки, после завершения работы сценария, дает результат, в соответствии с таблицей, приведенной выше, но только для Bash или sh. Под управлением csh или tcsh значения могут в некоторых случаях отличаться.

Примечания

[1]

Указание кода завершения за пределами установленного диапазона, приводит к возврату ошибочных кодов. Например, exit 3809 вернет код завершения, равный 225.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Приложение D. Подробное введение в операции ввода-вывода и перенаправление ввода-вывода

написано Stephane Chazelas и дополнено автором документа

Практически любая команда предполагает доступность 3-х файловых дескрипторов. Первый -- 0 (стандвртный ввод, stdin), доступный для чтения. И два других -- 1 (stdout) и 2 (stderr), доступные для записи.

Запись, типа ls 2>&1, означает временное перенаправление вывода, с устройства stderr на устройство stdout.

В соответствии с соглашениями, команды принимают ввод из файла с дескриптором 0 (stdin), выводят результат работы в файл с дескриптором 1 (stdout), а сообщения об ошибках -- в файл с дескриптором 2 (stderr). Если какой либо из этих трех дескрипторов окажется закрытым, то могут возникнуть определенные проблемы:

bash$ cat /etc/passwd >&-
cat: standard output: Bad file descriptor
     

К примеру, когда пользователь запускает xterm, то он сначала выполняет процедуру инициализации, а затем, перед запуском командной оболочки, xterm трижды открывает терминальные устройства (/dev/pts/<n>, или нечто подобное).

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

ls > /dev/null -- означает запуск команды ls с файловым дескриптором 1, присоединенным к устройству /dev/null.

bash$ lsof -a -p $$ -d0,1,2
COMMAND PID     USER   FD   TYPE DEVICE SIZE NODE NAME
 bash    363 bozo        0u   CHR  136,1         3 /dev/pts/1
 bash    363 bozo        1u   CHR  136,1         3 /dev/pts/1
 bash    363 bozo        2u   CHR  136,1         3 /dev/pts/1


bash$ exec 2> /dev/null
bash$ lsof -a -p $$ -d0,1,2
COMMAND PID     USER   FD   TYPE DEVICE SIZE NODE NAME
 bash    371 bozo        0u   CHR  136,1         3 /dev/pts/1
 bash    371 bozo        1u   CHR  136,1         3 /dev/pts/1
 bash    371 bozo        2w   CHR    1,3       120 /dev/null


bash$ bash -c 'lsof -a -p $$ -d0,1,2' | cat
COMMAND PID USER   FD   TYPE DEVICE SIZE NODE NAME
 lsof    379 root    0u   CHR  136,1         3 /dev/pts/1
 lsof    379 root    1w  FIFO    0,0      7118 pipe
 lsof    379 root    2u   CHR  136,1         3 /dev/pts/1


bash$ echo "$(bash -c 'lsof -a -p $$ -d0,1,2' 2>&1)"
COMMAND PID USER   FD   TYPE DEVICE SIZE NODE NAME
 lsof    426 root    0u   CHR  136,1         3 /dev/pts/1
 lsof    426 root    1w  FIFO    0,0      7520 pipe
 lsof    426 root    2w  FIFO    0,0      7520 pipe


Упражнение: Проанализируйте следующий сценарий.

#! /usr/bin/env bash

mkfifo /tmp/fifo1 /tmp/fifo2
while read a; do echo "FIFO1: $a"; done < /tmp/fifo1 &
exec 7> /tmp/fifo1
exec 8> >(while read a; do echo "FD8: $a, to fd7"; done >&7)

exec 3>&1
(
 (
  (
   while read a; do echo "FIFO2: $a"; done < /tmp/fifo2 | tee /dev/stderr | tee /dev/fd/4 | tee /dev/fd/5 | tee /dev/fd/6 >&7 &
   exec 3> /tmp/fifo2

   echo 1st, to stdout
   sleep 1
   echo 2nd, to stderr >&2
   sleep 1
   echo 3rd, to fd 3 >&3
   sleep 1
   echo 4th, to fd 4 >&4
   sleep 1
   echo 5th, to fd 5 >&5
   sleep 1                                                                                              
   echo 6th, through a pipe | sed 's/.*/PIPE: &, to fd 5/' >&5                                          
   sleep 1                                                                                              
   echo 7th, to fd 6 >&6                                                                                
   sleep 1                                                                                              
   echo 8th, to fd 7 >&7
   sleep 1                                                                                              
   echo 9th, to fd 8 >&8                                                                                
                                                                                                        
  ) 4>&1 >&3 3>&- | while read a; do echo "FD4: $a"; done 1>&3 5>&- 6>&-                                
 ) 5>&1 >&3 | while read a; do echo "FD5: $a"; done 1>&3 6>&-
) 6>&1 >&3 | while read a; do echo "FD6: $a"; done 3>&-                                                 
                                                                                                        
rm -f /tmp/fifo1 /tmp/fifo2


# Выясните, куда переназначены файловые дескрипторы каждой команды и подоболочки.

exit 0



Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Приложение E. Локализация

Возможность локализации сценариев Bash нигде в документации не описана.

Локализованные сценарии выводят текст на том языке, который используется системой, в соответствии с настройками. Пользователь Linux, живущий в Берлине (Германия), будет видеть сообщения на немецком языке, в то время как другой пользователь, проживающий в Берлине штата Мэриленд (США) -- на английском.

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

#!/bin/bash
# localized.sh

E_CDERROR=65

error()
{
  printf "$@" >&2
  exit $E_CDERROR
}

cd $var || error $"Can't cd to %s." "$var"
read -p $"Enter the value: " var
# ...


bash$ bash -D localized.sh
"Can't cd to %s."
"Enter the value: "
Это список всех текстовых сообщений, которые подлежат локализации. (Ключ -D выводит список строк в двойных кавычках, которым предшествует символ $, без запуска сценария на исполнение.)

bash$ bash --dump-po-strings localized.sh
#: a:6
 msgid "Can't cd to %s."
 msgstr ""
 #: a:7
 msgid "Enter the value: "
 msgstr ""
Ключ --dump-po-strings в Bash напоминает ключ -D, но выводит строки в формате "po", с помощью утилиты gettext.

Теперь построим файл language.po, для каждого языка, на которые предполагается перевести сообщения сценария. Например:

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

ru.po:

#: a:6
msgid "Can't cd to %s."
msgstr "Невозможно перейти в каталог %s."
#: a:7
msgid "Enter the value: "
msgstr "Введите число: "


Затем запустите msgfmt.

msgfmt -o localized.sh.mo ru.po

Перепишите получившийся файл localized.sh.mo в каталог /usr/share/locale/ru/LC_MESSAGES и добавьте в начало сценария строки:

TEXTDOMAINDIR=/usr/share/locale
TEXTDOMAIN=localized.sh


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

Note

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

#!/bin/bash
# localized.sh

E_CDERROR=65

error() {
  local format=$1
  shift
  printf "$(gettext -s "$format")" "$@" >&2
  exit $E_CDERROR
}
cd $var || error "Can't cd to %s." "$var"
read -p "$(gettext -s "Enter the value: ")" var
# ...


А переменные TEXTDOMAIN и TEXTDOMAINDIR, необходимо будет экспортировать в окружение.

---

Автор этого приложения: Stephane Chazelas.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Приложение F. История команд

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

История команд Bash:

  1. history

  2. fc



bash$ history
1  mount /mnt/cdrom
2  cd /mnt/cdrom
3  ls
     ...
             


Внутренние переменные Bash, связанные с историей команд:

  1. $HISTCMD

  2. $HISTCONTROL

  3. $HISTIGNORE

  4. $HISTFILE

  5. $HISTFILESIZE

  6. $HISTSIZE

  7. !!

  8. !$

  9. !#

  10. !N

  11. !-N

  12. !STRING

  13. !?STRING?

  14. ^STRING^string^



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

#!/bin/bash
# history.sh
# Попытка воспользоваться 'историей' команд в сценарии.

history

# На экран ничего не выводится.
# История команд не работает в сценариях.


bash$ ./history.sh
(ничего не выводится)
             



Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Приложение G. Пример файла .bashrc

Файл ~/.bashrc определяет поведение командной оболочки. Внимательное изучение этого примера поможет вам значительно продвинуться в понимании Bash.

Emmanuel Rouat представил следующий, очень сложный, файл .bashrc, написанный для операционной системы Linux. Предложения и замечания приветствуются.

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

Пример G-1. Пример файла .bashrc

#===============================================================
#
# ЛИЧНЫЙ ФАЙЛ $HOME/.bashrc для bash-2.05a (или выше)
#
# Время последней модификации: Втр Апр 15 20:32:34 CEST 2003
#
# Этот файл содержит настройки интерактивной командной оболочки.
# Здесь размещены определения псевдонимов, функций
# и других элементов Bash, таких как prompt (приглашение к вводу).
#
# Изначально, этот файл был создан в операционной системе Solaris,
# но позднее был переделан под Redhat
# --> Модифицирован под Linux.
# Большая часть кода, который находится здесь, была взята из
# Usenet (или Интернет).
# Этот файл содержит слишком много определений -- помните, это всего лишь пример.
#
#
#===============================================================

# --> Комментарии, добавленные автором HOWTO.
# --> И дополнены автором сценария Emmanuel Rouat :-)

#-----------------------------------
# Глобальные определения
#-----------------------------------

if [ -f /etc/bashrc ]; then
        . /etc/bashrc   # --> Прочитать настройки из /etc/bashrc, если таковой имеется.
fi

#-------------------------------------------------------------
# Настройка переменной $DISPLAY (если еще не установлена)
# Это срабатывает под linux - в вашем случае все может быть по другому....
# Проблема в том, что различные типы терминалов
# дают разные ответы на запрос 'who am i'......
# я не нашел 'универсального' метода
#-------------------------------------------------------------

function get_xserver ()
{
    case $TERM in
        xterm )
            XSERVER=$(who am i | awk '{print $NF}' | tr -d ')''(' )
            XSERVER=${XSERVER%%:*}
            ;;
        aterm | rxvt)
        # добавьте здесь свой код.....
            ;;
    esac
}

if [ -z ${DISPLAY:=""} ]; then
    get_xserver
    if [[ -z ${XSERVER}  || ${XSERVER} == $(hostname) || ${XSERVER} == "unix" ]]; then
        DISPLAY=":0.0"          # для локального хоста
    else
        DISPLAY=${XSERVER}:0.0  # для удаленного хоста
    fi
fi

export DISPLAY

#---------------
# Некоторые настройки
#---------------

ulimit -S -c 0          # Запрет на создание файлов coredump
set -o notify
set -o noclobber
set -o ignoreeof
set -o nounset
#set -o xtrace          # полезно для отладки

# Разрешающие настройки:
shopt -s cdspell
shopt -s cdable_vars
shopt -s checkhash
shopt -s checkwinsize
shopt -s mailwarn
shopt -s sourcepath
shopt -s no_empty_cmd_completion  # только для bash>=2.04
shopt -s cmdhist
shopt -s histappend histreedit histverify
shopt -s extglob

# Запрещающие настройки:
shopt -u mailwarn
unset MAILCHECK         # Я не желаю, чтобы командная оболочка сообщала мне о прибытии почты


export TIMEFORMAT=$'\nreal %3R\tuser %3U\tsys %3S\tpcpu %P\n'
export HISTIGNORE="&:bg:fg:ll:h"
export HOSTFILE=$HOME/.hosts    # Поместить список удаленных хостов в файл ~/.hosts



#-----------------------
# Greeting, motd etc...
#-----------------------

# Для начала определить некоторые цвета:
red='\e[0;31m'
RED='\e[1;31m'
blue='\e[0;34m'
BLUE='\e[1;34m'
cyan='\e[0;36m'
CYAN='\e[1;36m'
NC='\e[0m'              # No Color (нет цвета)
# --> Прекрасно. Имеет тот же эффект, что и "ansi.sys" в DOS.

# Лучше выглядит на черном фоне.....
echo -e "${CYAN}This is BASH ${RED}${BASH_VERSION%.*}${CYAN} - DISPLAY on ${RED}$DISPLAY${NC}\n"
date
if [ -x /usr/games/fortune ]; then
    /usr/games/fortune -s     # сделает наш день более интересным.... :-)
fi

function _exit()        # функция, запускающаяся при выходе из оболочки
{
    echo -e "${RED}Аста ла виста, бэби ${NC}"
}
trap _exit EXIT

#---------------
# Prompt
#---------------

if [[ "${DISPLAY#$HOST}" != ":0.0" &&  "${DISPLAY}" != ":0" ]]; then
    HILIT=${red}   # на удаленной системе: prompt будет частично красным
else
    HILIT=${cyan}  # на локальной системе: prompt будет частично циановым
fi

#  --> Замените \W на \w в функциях ниже
#+ --> чтобы видеть в оболочке полный путь к текущему каталогу.

function fastprompt()
{
    unset PROMPT_COMMAND
    case $TERM in
        *term | rxvt )
            PS1="${HILIT}[\h]$NC \W > \[\033]0;\${TERM} [\u@\h] \w\007\]" ;;
        linux )
            PS1="${HILIT}[\h]$NC \W > " ;;
        *)
            PS1="[\h] \W > " ;;
    esac
}

function powerprompt()
{
    _powerprompt()
    {
        LOAD=$(uptime|sed -e "s/.*: \([^,]*\).*/\1/" -e "s/ //g")
    }

    PROMPT_COMMAND=_powerprompt
    case $TERM in
        *term | rxvt  )
            PS1="${HILIT}[\A \$LOAD]$NC\n[\h \#] \W > \[\033]0;\${TERM} [\u@\h] \w\007\]" ;;
        linux )
            PS1="${HILIT}[\A - \$LOAD]$NC\n[\h \#] \w > " ;;
        * )
            PS1="[\A - \$LOAD]\n[\h \#] \w > " ;;
    esac
}

powerprompt     # это prompt по-умолчанию - может работать довольно медленно
                # Если это так, то используйте fastprompt....

#===============================================================
#
# ПСЕВДОНИМЫ И ФУНКЦИИ
#
# Возможно некоторые из функций, приведенных здесь, окажутся для вас слишком большими,
# но на моей рабочей станции установлено 512Mb ОЗУ, так что.....
# Если пожелаете уменьшить размер этого файла, то можете оформить эти функции
# в виде отдельных сценариев.
#
# Большинство функций были взяты, почти без переделки, из примеров
# к bash-2.04.
#
#===============================================================

#-------------------
# Псевдонимы
#-------------------

alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'
# -> Предотвращает случайное удаление файлов.
alias mkdir='mkdir -p'

alias h='history'
alias j='jobs -l'
alias r='rlogin'
alias which='type -all'
alias ..='cd ..'
alias path='echo -e ${PATH//:/\\n}'
alias print='/usr/bin/lp -o nobanner -d $LPDEST'   # Предполагается, что LPDEST определен
alias pjet='enscript -h -G -fCourier9 -d $LPDEST'  # Печать через enscript
alias background='xv -root -quit -max -rmode 5'    # Положить картинку в качестве фона
alias du='du -kh'
alias df='df -kTh'

# Различные варианты 'ls' (предполагается, что установлена GNU-версия ls)
alias la='ls -Al'               # показать скрытые файлы
alias ls='ls -hF --color'       # выделить различные типы файлов цветом
alias lx='ls -lXB'              # сортировка по расширению
alias lk='ls -lSr'              # сортировка по размеру
alias lc='ls -lcr'              # сортировка по времени изменения
alias lu='ls -lur'              # сортировка по времени последнего обращения
alias lr='ls -lR'               # рекурсивный обход подкаталогов
alias lt='ls -ltr'              # сортировка по дате
alias lm='ls -al |more'         # вывод через 'more'
alias tree='tree -Csu'          # альтернатива 'ls'

# подготовка 'less'
alias more='less'
export PAGER=less
export LESSCHARSET='latin1'
export LESSOPEN='|/usr/bin/lesspipe.sh %s 2>&-' # если существует lesspipe.sh
export LESS='-i -N -w  -z-4 -g -e -M -X -F -R -P%t?f%f \
:stdin .?pb%pb\%:?lbLine %lb:?bbByte %bb:-...'

# проверка правописания - настоятельно рекомендую :-)
alias xs='cd'
alias vf='cd'
alias moer='more'
alias moew='more'
alias kk='ll'

#----------------
# добавим немножко "приятностей"
#----------------

function xtitle ()
{
    case "$TERM" in
        *term | rxvt)
            echo -n -e "\033]0;$*\007" ;;
        *)
            ;;
    esac
}

# псевдонимы...
alias top='xtitle Processes on $HOST && top'
alias make='xtitle Making $(basename $PWD) ; make'
alias ncftp="xtitle ncFTP ; ncftp"

# .. и функции
function man ()
{
    for i ; do
        xtitle The $(basename $1|tr -d .[:digit:]) manual
        command man -F -a "$i"
    done
}

function ll(){ ls -l "$@"| egrep "^d" ; ls -lXB "$@" 2>&-| egrep -v "^d|total "; }
function te()  # "обертка" вокруг xemacs/gnuserv
{
    if [ "$(gnuclient -batch -eval t 2>&-)" == "t" ]; then
        gnuclient -q "$@";
    else
        ( xemacs "$@" &);
    fi
}

#-----------------------------------
# Функции для работы с файлами и строками:
#-----------------------------------

# Поиск файла по шаблону:
function ff() { find . -type f -iname '*'$*'*' -ls ; }
# Поиск файла по шаблону в $1 и запуск команды в $2 с ним:
function fe() { find . -type f -iname '*'$1'*' -exec "${2:-file}" {} \;  ; }
# поиск строки по файлам:
function fstr()
{
    OPTIND=1
    local case=""
    local usage="fstr: поиск строки в файлах.
Порядок использования: fstr [-i] \"шаблон\" [\"шаблон_имени_файла\"] "
    while getopts :it opt
    do
        case "$opt" in
        i) case="-i " ;;
        *) echo "$usage"; return;;
        esac
    done
    shift $(( $OPTIND - 1 ))
    if [ "$#" -lt 1 ]; then
        echo "$usage"
        return;
    fi
    local SMSO=$(tput smso)
    local RMSO=$(tput rmso)
    find . -type f -name "${2:-*}" -print0 | xargs -0 grep -sn ${case} "$1" 2>&- | \
sed "s/$1/${SMSO}\0${RMSO}/gI" | more
}

function cuttail() # удалить последние n строк в файле, по-умолчанию 10
{
    nlines=${2:-10}
    sed -n -e :a -e "1,${nlines}!{P;N;D;};N;ba" $1
}

function lowercase()  # перевести имя файла в нижний регистр
{
    for file ; do
        filename=${file##*/}
        case "$filename" in
        */*) dirname==${file%/*} ;;
        *) dirname=.;;
        esac
        nf=$(echo $filename | tr A-Z a-z)
        newname="${dirname}/${nf}"
        if [ "$nf" != "$filename" ]; then
            mv "$file" "$newname"
            echo "lowercase: $file --> $newname"
        else
            echo "lowercase: имя файла $file не было изменено."
        fi
    done
}

function swap()         # меняет 2 файла местами
{
    local TMPFILE=tmp.$$
    mv "$1" $TMPFILE
    mv "$2" "$1"
    mv $TMPFILE "$2"
}


#-----------------------------------
# Функции для работы с процессами/системой:
#-----------------------------------

function my_ps() { ps $@ -u $USER -o pid,%cpu,%mem,bsdtime,command ; }
function pp() { my_ps f | awk '!/awk/ && $0~var' var=${1:-".*"} ; }

# Эта функция является грубым аналогом 'killall' в linux
# но не эквивалентна (насколько я знаю) 'killall' в Solaris
function killps()   # "Прибить" процесс по его имени
{
    local pid pname sig="-TERM"   # сигнал, рассылаемый по-умолчанию
    if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
        echo "Порядок использования: killps [-SIGNAL] шаблон_имени_процесса"
        return;
    fi
    if [ $# = 2 ]; then sig=$1 ; fi
    for pid in $(my_ps| awk '!/awk/ && $0~pat { print $1 }' pat=${!#} ) ; do
        pname=$(my_ps | awk '$1~var { print $5 }' var=$pid )
        if ask "Послать сигнал $sig процессу $pid <$pname>?"
            then kill $sig $pid
        fi
    done
}

function my_ip() # IP адрес
{
    MY_IP=$(/sbin/ifconfig ppp0 | awk '/inet/ { print $2 } ' | sed -e s/addr://)
    MY_ISP=$(/sbin/ifconfig ppp0 | awk '/P-t-P/ { print $3 } ' | sed -e s/P-t-P://)
}

function ii()   # Дополнительные сведения о системе
{
    echo -e "\nВы находитесь на ${RED}$HOST"
    echo -e "\nДополнительная информация:$NC " ; uname -a
    echo -e "\n${RED}В системе работают пользователи:$NC " ; w -h
    echo -e "\n${RED}Дата:$NC " ; date
    echo -e "\n${RED}Время, прошедшее с момента последней перезагрузки :$NC " ; uptime
    echo -e "\n${RED}Память :$NC " ; free
    my_ip 2>&- ;
    echo -e "\n${RED}IP адрес:$NC" ; echo ${MY_IP:-"Соединение не установлено"}
    echo -e "\n${RED}Адрес провайдера (ISP):$NC" ; echo ${MY_ISP:-"Соединение не установлено"}
    echo
}

# Разные утилиты:

function repeat()       # повторить команду n раз
{
    local i max
    max=$1; shift;
    for ((i=1; i <= max ; i++)); do  # --> C-подобный синтаксис
        eval "$@";
    done
}

function ask()
{
    echo -n "$@" '[y/n] ' ; read ans
    case "$ans" in
        y*|Y*) return 0 ;;
        *) return 1 ;;
    esac
}

#=========================================================================
#
# ПРОГРАММНЫЕ ДОПОЛНЕНИЯ - ТОЛЬКО НАЧИНАЯ С ВЕРСИИ BASH-2.04
# Большая часть дополнений взята из докуентации к bash 2.05  и из
# пакета 'Bash completion' (http://www.caliban.org/bash/index.shtml#completion)
# автор -- Ian McDonalds
# Фактически, у вас должен стоять bash-2.05a
#
#=========================================================================

if [ "${BASH_VERSION%.*}" \< "2.05" ]; then
    echo "Вам необходимо обновиться до версии 2.05"
    return
fi

shopt -s extglob        # необходимо
set +o nounset          # иначе некоторые дополнения не будут работать

complete -A hostname   rsh rcp telnet rlogin r ftp ping disk
complete -A export     printenv
complete -A variable   export local readonly unset
complete -A enabled    builtin
complete -A alias      alias unalias
complete -A function   function
complete -A user       su mail finger

complete -A helptopic  help
complete -A shopt      shopt
complete -A stopped -P '%' bg
complete -A job -P '%'     fg jobs disown

complete -A directory  mkdir rmdir
complete -A directory   -o default cd

# Архивация
complete -f -o default -X '*.+(zip|ZIP)'  zip
complete -f -o default -X '!*.+(zip|ZIP)' unzip
complete -f -o default -X '*.+(z|Z)'      compress
complete -f -o default -X '!*.+(z|Z)'     uncompress
complete -f -o default -X '*.+(gz|GZ)'    gzip
complete -f -o default -X '!*.+(gz|GZ)'   gunzip
complete -f -o default -X '*.+(bz2|BZ2)'  bzip2
complete -f -o default -X '!*.+(bz2|BZ2)' bunzip2
# Postscript,pdf,dvi.....
complete -f -o default -X '!*.ps'  gs ghostview ps2pdf ps2ascii
complete -f -o default -X '!*.dvi' dvips dvipdf xdvi dviselect dvitype
complete -f -o default -X '!*.pdf' acroread pdf2ps
complete -f -o default -X '!*.+(pdf|ps)' gv
complete -f -o default -X '!*.texi*' makeinfo texi2dvi texi2html texi2pdf
complete -f -o default -X '!*.tex' tex latex slitex
complete -f -o default -X '!*.lyx' lyx
complete -f -o default -X '!*.+(htm*|HTM*)' lynx html2ps
# Multimedia
complete -f -o default -X '!*.+(jp*g|gif|xpm|png|bmp)' xv gimp
complete -f -o default -X '!*.+(mp3|MP3)' mpg123 mpg321
complete -f -o default -X '!*.+(ogg|OGG)' ogg123



complete -f -o default -X '!*.pl'  perl perl5

# Эти 'универсальные' дополнения работают тогда, когда команды вызываются
# с, так называемыми, 'длинными ключами', например: 'ls --all' вместо 'ls -a'

_get_longopts ()
{
    $1 --help | sed  -e '/--/!d' -e 's/.*--\([^[:space:].,]*\).*/--\1/'| \
grep ^"$2" |sort -u ;
}

_longopts_func ()
{
    case "${2:-*}" in
        -*)     ;;
        *)      return ;;
    esac

    case "$1" in
        \~*)    eval cmd="$1" ;;
        *)      cmd="$1" ;;
    esac
    COMPREPLY=( $(_get_longopts ${1} ${2} ) )
}
complete  -o default -F _longopts_func configure bash
complete  -o default -F _longopts_func wget id info a2ps ls recode


_make_targets ()
{
    local mdef makef gcmd cur prev i

    COMPREPLY=()
    cur=${COMP_WORDS[COMP_CWORD]}
    prev=${COMP_WORDS[COMP_CWORD-1]}

    # Если аргумент prev это -f, то вернуть возможные варианты имен файлов.
    # будем великодушны и вернем несколько вариантов
    # `makefile Makefile *.mk'
    case "$prev" in
        -*f)    COMPREPLY=( $(compgen -f $cur ) ); return 0;;
    esac

    # Если запрошены возможные ключи, то вернуть ключи posix
    case "$cur" in
        -)      COMPREPLY=(-e -f -i -k -n -p -q -r -S -s -t); return 0;;
    esac

    # попробовать передать make `makefile' перед тем как попробовать передать `Makefile'
    if [ -f makefile ]; then
        mdef=makefile
    elif [ -f Makefile ]; then
        mdef=Makefile
    else
        mdef=*.mk
    fi

    # прежде чем просмотреть "цели", убедиться, что имя makefile было задано
    # ключом -f
    for (( i=0; i < ${#COMP_WORDS[@]}; i++ )); do
        if [[ ${COMP_WORDS[i]} == -*f ]]; then
            eval makef=${COMP_WORDS[i+1]}
            break
        fi
    done

        [ -z "$makef" ] && makef=$mdef

    # Если задан шаблон поиска, то ограничиться
    # этим шаблоном
    if [ -n "$2" ]; then gcmd='grep "^$2"' ; else gcmd=cat ; fi

    # если мы не желаем использовать *.mk, то необходимо убрать cat и использовать
    # test -f $makef с перенаправлением ввода
    COMPREPLY=( $(cat $makef 2>/dev/null | awk 'BEGIN {FS=":"} /^[^.#   ][^=]*:/ {print $1}' | tr -s ' ' '\012' | sort -u | eval $gcmd ) )
}

complete -F _make_targets -X '+($*|*.[cho])' make gmake pmake


# cvs(1) completion
_cvs ()
{
    local cur prev
    COMPREPLY=()
    cur=${COMP_WORDS[COMP_CWORD]}
    prev=${COMP_WORDS[COMP_CWORD-1]}

    if [ $COMP_CWORD -eq 1 ] || [ "${prev:0:1}" = "-" ]; then
        COMPREPLY=( $( compgen -W 'add admin checkout commit diff \
        export history import log rdiff release remove rtag status \
        tag update' $cur ))
    else
        COMPREPLY=( $( compgen -f $cur ))
    fi
    return 0
}
complete -F _cvs cvs

_killall ()
{
    local cur prev
    COMPREPLY=()
    cur=${COMP_WORDS[COMP_CWORD]}

    # получить список процессов
    COMPREPLY=( $( /usr/bin/ps -u $USER -o comm  | \
        sed -e '1,1d' -e 's#[]\[]##g' -e 's#^.*/##'| \
        awk '{if ($0 ~ /^'$cur'/) print $0}' ))

    return 0
}

complete -F _killall killall killps


# Функция обработки мета-команд
# В настоящее время недостаточно отказоустойчива (например, mount и umount
# обрабатываются некорректно), но все еще актуальна. Автор Ian McDonald, изменена мной.

_my_command()
{
    local cur func cline cspec

    COMPREPLY=()
    cur=${COMP_WORDS[COMP_CWORD]}

    if [ $COMP_CWORD = 1 ]; then
        COMPREPLY=( $( compgen -c $cur ) )
    elif complete -p ${COMP_WORDS[1]} &>/dev/null; then
        cspec=$( complete -p ${COMP_WORDS[1]} )
        if [ "${cspec%%-F *}" != "${cspec}" ]; then
            # complete -F <function>
            #
            # COMP_CWORD and COMP_WORDS() доступны на запись,
            # так что мы можем установить их перед тем,
            # как передать их дальше

            # уменьшить на 1 текущий номер лексемы
            COMP_CWORD=$(( $COMP_CWORD - 1 ))
            # получить имя функции
            func=${cspec#*-F }
            func=${func%% *}
            # получить командную строку, исключив первую команду
            cline="${COMP_LINE#$1 }"
            # разбить на лексемы и поместить в массив
                COMP_WORDS=( $cline )
            $func $cline
        elif [ "${cspec#*-[abcdefgjkvu]}" != "" ]; then
            # complete -[abcdefgjkvu]
            #func=$( echo $cspec | sed -e 's/^.*\(-[abcdefgjkvu]\).*$/\1/' )
            func=$( echo $cspec | sed -e 's/^complete//' -e 's/[^ ]*$//' )
            COMPREPLY=( $( eval compgen $func $cur ) )
        elif [ "${cspec#*-A}" != "$cspec" ]; then
            # complete -A <type>
            func=${cspec#*-A }
        func=${func%% *}
        COMPREPLY=( $( compgen -A $func $cur ) )
        fi
    else
        COMPREPLY=( $( compgen -f $cur ) )
    fi
}


complete -o default -F _my_command nohup exec eval trace truss strace sotruss gdb
complete -o default -F _my_command command type which man nice

# Локальные переменные:
# mode:shell-script
# sh-shell:bash
# Конец:

Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Приложение H. Преобразование пакетных (*.bat) файлов DOS в сценарии командной оболочки

Большое число программистов начинало изучать скриптовые языки на PC, работающих под управлением DOS. Даже на этом "калеке" удавалось создавать неплохие сценарии, хотя это и требовало значительных усилий. Иногда еще возникает потребность в переносе пекетных файлов DOS на платформу UNIX, в виде сценариев командной оболочки. Обычно это не сложно, поскольку набор операторов, доступных в DOS, представляет из себя ограниченное подмножество эквивалентных команд, доступных в командной оболочке.

Таблица H-1. Ключевые слова/переменные/операторы пакетных файлов DOS и их аналоги командной оболочки

Операторы пакетных файлов Эквивалентные команды в UNIX Описание
% $ префикс аргументов командной строки
/ - признак ключа (опции)
\ / разделитель имен каталогов в пути
== = (равно) сравнение строк
!==! != (не равно) сравнение строк
| | конвейер (канал)
@ set +v не выводить текущую команду
* * "шаблонный символ" в имени файла
> > перенаправление (с удалением существующего файла)
>> >> перенаправление (с добавлением в конец существующего файла)
< < перенаправление ввода stdin
%VAR% $VAR переменная окружения
REM # комментарий
NOT ! отрицание последующего условия
NUL /dev/null "черная дыра" для того, чтобы "спрятать" вывод команды
ECHO echo вывод (в Bash имеет большое число опций)
ECHO. echo вывод пустой строки
ECHO OFF set +v не выводить последующие команды
FOR %%VAR IN (LIST) DO for var in [list]; do цикл "for"
:LABEL эквивалент отсутствует (нет необходимости) метка
GOTO эквивалент отсутствует (используйте функции) переход по заданной метке
PAUSE sleep пауза, или ожидание, в течение заданного времени
CHOICE case или select выбор из меню
IF if условный оператор if
IF EXIST FILENAME if [ -e filename ] проверка существования файла
IF !%N==! if [ -z "$N" ] Проверка: параметр "N" отсутствует
CALL source или . (оператор "точка") "подключение" другого сценария
COMMAND /C source или . (оператор "точка") "подключение" другого сценария (то же, что и CALL)
SET export установить переменную окружения
SHIFT shift сдвиг списка аргументов уомандной строки влево
SGN -lt или -gt знак (целого числа)
ERRORLEVEL $? код завершения
CON stdin "консоль" (stdin)
PRN /dev/lp0 устройство принтера
LPT1 /dev/lp0 устройство принтера
COM1 /dev/ttyS0 первый последовательный порт

Пакетные файлы обычно содержат вызовы команд DOS. Они должны быть заменены эквивалентными командами UNIX.

Таблица H-2. Команды DOS и их эквиваленты в UNIX

Команды DOS Эувивалент в UNIX Описание
ASSIGN ln ссылка на файл или каталог
ATTRIB chmod изменить атрибуты файла (права доступа)
CD cd сменить каталог
CHDIR cd сменить каталог
CLS clear очистить экран
COMP diff, comm, cmp сравнить файлы
COPY cp скопировать файл
Ctl-C Ctl-C прервать исполнение сценария
Ctl-Z Ctl-D EOF (конец-файла)
DEL rm удалить файл(ы)
DELTREE rm -rf удалить каталог с подкаталогами
DIR ls -l вывести содержимое каталога
ERASE rm удалить файл(ы)
EXIT exit завершить текущий процесс
FC comm, cmp сравнить файлы
FIND grep найти строку в файлах
MD mkdir создать каталог
MKDIR mkdir создать каталог
MORE more постраничный вывод
MOVE mv переместить
PATH $PATH путь поиска исполняемых файлов
REN mv переименовать (переместить)
RENAME mv переименовать (переместить)
RD rmdir удалить каталог
RMDIR rmdir удалить каталог
SORT sort отсортировать файл
TIME date вывести системное время
TYPE cat вывести содержимое файла на stdout
XCOPY cp (расширенная команда) скопировать файл
Note

Фактически, команды и операторы командной оболочки UNIX имеют огромное количество дополнительных опций, расширяющих их функциональность, по сравнению с их эквивалентами в DOS. В большинстве своем, пакетные файлы DOS предполагают наличие вспомогательных утилит, таких как ask.com ("увечный" аналог UNIX-вого read).

DOS поддерживает крайне ограниченный набор шаблонных символов, учавствующих в операциях подстановки имен файлов, распознавая только два символа -- * и ?.

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

Пример H-1. VIEWDATA.BAT: пакетный файл DOS

REM VIEWDATA

REM INSPIRED BY AN EXAMPLE IN "DOS POWERTOOLS"
REM                           BY PAUL SOMERSON


@ECHO OFF

IF !%1==! GOTO VIEWDATA
REM  IF NO COMMAND-LINE ARG...
FIND "%1" C:\BOZO\BOOKLIST.TXT
GOTO EXIT0
REM  PRINT LINE WITH STRING MATCH, THEN EXIT.

:VIEWDATA
TYPE C:\BOZO\BOOKLIST.TXT | MORE
REM  SHOW ENTIRE FILE, 1 PAGE AT A TIME.

:EXIT0

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

Пример H-2. viewdata.sh: Результат преобразования VIEWDATA.BAT в сценарий командной оболочки

#!/bin/bash
# Результат преобразования пакетного файла VIEWDATA.BAT в сценарий командной оболочки.

DATAFILE=/home/bozo/datafiles/book-collection.data
ARGNO=1

# @ECHO OFF       Эта команда здесь не нужна.

if [ $# -lt "$ARGNO" ]    # IF !%1==! GOTO VIEWDATA
then
  less $DATAFILE          # TYPE C:\MYDIR\BOOKLIST.TXT | MORE
else
  grep "$1" $DATAFILE     # FIND "%1" C:\MYDIR\BOOKLIST.TXT
fi

exit 0                    # :EXIT0

# операторы перехода GOTO, метки и прочий "мусор" больше не нужны.
# Результат преобразования стал короче, чище и понятнее,

На сайте Тэда Дэвиса (Ted Davis) Shell Scripts on the PC, вы найдете большое число руководств по созданию пакетных файлов в DOS. Определенно, его изобретательность будет вам полезна, при создании ваших сценариев.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Приложение I. Упражнения

I.1. Анализ сценариев

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

#!/bin/bash

MAX=10000


  for((nr=1; nr<$MAX; nr++))
  do

    let "t1 = nr % 5"
    if [ "$t1" -ne 3 ]
    then
      continue
    fi

    let "t2 = nr % 7"
    if [ "$t2" -ne 4 ]
    then
      continue
    fi

    let "t3 = nr % 9"
    if [ "$t3" -ne 5 ]
    then
      continue
    fi

  break   # Что произойдет, если закомментировать эту строку? Почему?

  done

  echo "Число = $nr"


exit 0


---

Читатель прислал следующий кусок кода.

while read LINE
do
  echo $LINE
done < `tail -f /var/log/messages`
Он предполагал написать сценарий, который отслеживал бы изменения в системном журнале /var/log/messages. К сожалению, этот код "зависает" и не делает ничего полезного. Почему? Найдите ошибку и исправьте ее (подсказка: вместо операции перенаправления stdin в цикл, попробуйте использовать конвейерную обработку).

---

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

Измените сценарий таким образом, чтобы он мог принимать начальную установку "поколения 0" из любого текстового файла. Сценарий должен считать первые $ROW*$COL символов, и на место гласных вставлять "живые особи". Подсказка: не забудьте преобразовать пробелы в символы подчеркивания.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад  

Приложение J. Авторские права

Авторские права на книгу "Advanced Bash-Scripting Guide", принадлежат Менделю Куперу (Mendel Cooper). Этот документ может распространяться исключительно на условиях Open Publication License (версия 1.0 или выше), http://www.opencontent.org/openpub/. Соблюдение следующих пунктов лицензии обязательно.

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

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

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

  1. Некоммерческих организаций, таких как Linux Documentation Project и Sunsite.

  2. Не "запятнавших" себя дистрибутивостроителей Linux, таких как Debian, Red Hat, Mandrake и других.

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

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

Права на коммерческое распространение книги могут быть получены у автора.

Автор произвел этот документ в соответствии с буквой и духом LDP Manifesto.

Hyun Jin Cha завершил перевод на Корейский язык версию 1.0.11 этой книги. Переводы на Испанский, Португальский, Французский, Немецкий, Итальянский и Китайский языки находятся на стадии реализации. Если вы изъявите желание перевести этот документ на другой язык, то можете свободно выполнить этот перевод, основываясь на условиях, заявленных выше. В этом случае, автор хотел бы, чтобы его поставили в известность.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Литература

Edited by Peter Denning, Computers Under Attack: Intruders, Worms, and Viruses, ACM Press, 1990, 0-201-53067-8.

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

*

Dale Dougherty and Arnold Robbins, Sed and Awk, 2nd edition, O'Reilly and Associates, 1997, 1-156592-225-5.

Чтобы раскрыть всю мощь командной оболочки, вам наверняка потребуется знакомство с sed и awk. Это обычный учебник. Здесь вы найдете превосходное введение в "регулярные выражения". Обязательно прочитайте эту книгу.

*

Aeleen Frisch, Essential System Administration, 3rd edition, O'Reilly and Associates, 2002, 0-596-00343-9.

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

*

Stephen Kochan and Patrick Woods, Unix Shell Programming, Hayden, 1990, 067248448X.

Стандартный справочник, хотя немного устаревший.

*

Neil Matthew and Richard Stones, Beginning Linux Programming, Wrox Press, 1996, 1874416680.

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

*

Herbert Mayer, Advanced C Programming on the IBM PC, Windcrest Books, 1989, 0830693637.

Замечательная книга по алгоритмам и практическому программированию.

*

David Medinets, Unix Shell Programming Tools, McGraw-Hill, 1999, 0070397333.

Отличная книга по программированию в командной оболочке, с примерами, и кратким введением в Tcl и Perl.

*

Cameron Newham and Bill Rosenblatt, Learning the Bash Shell, 2nd edition, O'Reilly and Associates, 1998, 1-56592-347-2.

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

*

Anatole Olczak, Bourne Shell Quick Reference Guide, ASP, Inc., 1991, 093573922X.

Очень удобный карманный справочник, несмотря на недостатки, при охвате специфичных свойств Bash.

*

Jerry Peek, Tim O'Reilly, and Mike Loukides, Unix Power Tools, 2nd edition, O'Reilly and Associates, Random House, 1997, 1-56592-260-3.

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

*

Clifford Pickover, Computers, Pattern, Chaos, and Beauty, St. Martin's Press, 1990, 0-312-04123-3.

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

*

George Polya, How To Solve It, Princeton University Press, 1973, 0-691-02356-5.

Классический учебник по методам решения задач.

*

Arnold Robbins, Bash Reference Card, SSC, 1998, 1-58731-010-5.

Замечательный карманный справочник по Bash. Стоит всего $4.95, но также доступен для свободного скачивания on-line в формате PDF.

*

Arnold Robbins, Effective Awk Programming, Free Software Foundation / O'Reilly and Associates, 2000, 1-882114-26-4.

Самое лучшее учебное руководство и справочник по awk. Свободная электронная версия книги включена в состав документации к awk. Печатное издание последней версии доступно на сайте O'Reilly and Associates.

Эта книга служила источником вдохновения для автора этой книги.

*

Bill Rosenblatt, Learning the Korn Shell, O'Reilly and Associates, 1993, 1-56592-054-6.

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

*

Paul Sheer, LINUX: Rute User's Tutorial and Exposition, 1st edition, , 2002, 0-13-033351-4.

Очень хорошее введение в системное администрирование Linux.

Эта книга доступна в on-line.

*

Ellen Siever and the staff of O'Reilly and Associates, Linux in a Nutshell, 2nd edition, O'Reilly and Associates, 1999, 1-56592-585-8.

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

*

The UNIX CD Bookshelf, 3rd edition, O'Reilly and Associates, 2003, 0-596-00392-7.

Сборник из 7-ми книг по UNIX на CD ROM. В состав сборника входят такие книги, как UNIX Power Tools, Sed and Awk и Learning the Korn Shell. Полный набор необходимых справочных и учебных материалов, который вам только может понадобиться. Стоит примерно $130.

*

Книги издательства O'Reilly, посвященные Perl.

---

Ben Okopnik опубликовал серию отличных статей introductory Bash scripting в выпусках 53, 54, 55, 57 и 59 на сайте Linux Gazette , и статью "The Deep, Dark Secrets of Bash" в выпуске 56.

Chet Ramey bash - The GNU Shell -- серия статей в 3 и 4 выпусках Linux Journal, Июль-Август 1994.

Chet Ramey Bash F.A.Q.

Ed Schaefer Shell Corner на Unix Review.

Примеры сценариев: Lucc's Shell Scripts .

Примеры сценариев: SHELLdorado .

Примеры сценариев: Noah Friedman's script site.

Steve Parker Shell Programming Stuff.

Примеры сценариев: SourceForge Snippet Library - shell scripts.

Giles Orr Bash-Prompt HOWTO.

Замечательное руководство по регулярным выражениям, sed и awk The UNIX Grymoire.

Eric Pement sed resources page.

The GNU gawk reference manual (gawk -- GNU-версия awk для ОС Linux и BSD).

Trent Fisher groff tutorial.

Mark Komarinski Printing-Usage HOWTO.

Rick Hohensee osimpa -- ассемблер для процессора i386, написан полностью на Bash.

Rocky Bernstein ведет разработку "полнофункционального" отладчика для Bash.

---

Отличное руководство "Bash Reference Manual", авторы Chet Ramey и Brian Fox, распространяется в составе пакета "bash-2-doc" (доступен как rpm). В этом пакете вы найдете особенно поучительные примеры.

Группа новостей comp.os.unix.shell.

Страницы руководства man по bash и bash2, date, expect, expr, find, grep, gzip, ln, patch, tar, tr, bc, xargs. Странички info по bash, dd, m4, gawk и sed.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

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

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

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

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


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

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

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

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

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


Caution

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

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

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

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

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


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


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

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

mkdir 'dir with trailing newline
'

cd 'dir with trailing newline
'

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

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





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

Спасибо S.C.


Caution

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

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

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

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

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


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


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

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

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


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


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


Caution

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

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

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


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

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

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


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

exit 0

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

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

Пример 14-2. Запись результатов выполнения цикла в переменную

#!/bin/bash
# csubloop.sh: Запись результатов выполнения цикла в переменную

variable1=`for i in 1 2 3 4 5
do
  echo -n "$i"                 #  Здесь 'echo' -- это ключевой момент
done`

echo "variable1 = $variable1"  # variable1 = 12345


i=0
variable2=`while [ "$i" -lt 10 ]
do
  echo -n "$i"                 # Опять же, команда 'echo' просто необходима.
  let "i += 1"                 # Увеличение на 1.
done`

echo "variable2 = $variable2"  # variable2 = 0123456789

exit 0
Note

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

output=$(sed -n /"$1"/p $file)   # К примеру из "grp.sh".

# Запись в переменную содержимого текстового файла.
File_contents1=$(cat $file1)
File_contents2=$(<$file2)        # Bash допускает и такую запись.


Примеры подстановки команд в сценариях:

  1. Пример 10-7

  2. Пример 10-26

  3. Пример 9-26

  4. Пример 12-2

  5. Пример 12-15

  6. Пример 12-12

  7. Пример 12-39

  8. Пример 10-13

  9. Пример 10-10

  10. Пример 12-24

  11. Пример 16-7

  12. Пример A-19

  13. Пример 27-1

  14. Пример 12-32

  15. Пример 12-33

  16. Пример 12-34



Примечания

[1]

Замещающая команда может быть внешней системной командой, внутренней (встроенной) командой или даже функцией в сценарии.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 15. Арифметические подстановки

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

Вариации

Арифметические подстановки в обратных одиночных кавычках (часто используются совместно с командой expr)
z=`expr $z + 3`            # Команда 'expr' вычисляет значение выражения.


Арифметические подстановки в двойных круглых скобках, и предложение let

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

z=$(($z+3))
# $((EXPRESSION)) -- это подстановка арифметического выражения.  #  Не путайте с
                                                                 #+ подстановкой команд.

let z=z+3
let "z += 3"  # Кавычки позволяют вставляьб пробелы и специальные операторы.
#  Оператор 'let' вычисляет арифметическое выражение,
#+ это не подстановка арифметического выражения.
Все вышеприведенные примеры эквивалентны. Вы можете использовать любую из этих форм записи "по своему вкусу".

Примеры арифметических подстановок в сценариях:

  1. Пример 12-6

  2. Пример 10-14

  3. Пример 25-1

  4. Пример 25-6

  5. Пример A-19




Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 16. Перенаправление ввода/вывода

В системе по-умолчанию всегда открыты три "файла" -- stdin (клавиатура), stdout (экран) и stderr (вывод сообщений об ошибках на экран). Эти, и любые другие открытые файлы, могут быть перенапрвлены. В данном случае, термин "перенаправление" означает получить вывод из файла, команды, программы, сценария или даже отдельного блока в сценарии (см. Пример 3-1 и Пример 3-2) и передать его на вход в другой файл, команду, программу или сценарий.

С каждым открытым файлом связан дескриптор файла. [1] Дескрипторы файлов stdin, stdout и stderr -- 0, 1 и 2, соответственно. При открытии дополнительных файлов, дескрипторы с 3 по 9 остаются незанятыми. Иногда дополнительные дескрипторы могут сослужить неплохую службу, временно сохраняя в себе ссылку на stdin, stdout или stderr. [2] Это упрощает возврат дескрипторов в нормальное состояние после сложных манипуляций с перенаправлением и перестановками (см. Пример 16-1).

   COMMAND_OUTPUT >
      # Перенаправление stdout (вывода) в файл.
      # Если файл отсутствовал, то он создется, иначе -- перезаписывается.

      ls -lR > dir-tree.list
      # Создает файл, содержащий список дерева каталогов.

   : > filename
      # Операция > усекает файл "filename" до нулевой длины.
      # Если до выполнения операции файла не существовало,
      # то создается новый файл с нулевой длиной (тот же эффект дает команда 'touch').
      # Символ : выступает здесь в роли местозаполнителя, не выводя ничего.

   > filename
      # Операция > усекает файл "filename" до нулевой длины.
      # Если до выполнения операции файла не существовало,
      # то создается новый файл с нулевой длиной (тот же эффект дает команда 'touch').
      # (тот же результат, что и выше -- ": >", но этот вариант неработоспособен
      # в некоторых командных оболочках.)

   COMMAND_OUTPUT >>
      # Перенаправление stdout (вывода) в файл.
      # Создает новый файл, если он отсутствовал, иначе -- дописывает в конец файла.


      # Однострочные команды перенаправления
      # (затрагивают только ту строку, в которой они встречаются):
      # --------------------------------------------------------------------

   1>filename
      # Перенаправление вывода (stdout) в файл "filename".
   1>>filename
      # Перенаправление вывода (stdout) в файл "filename", файл открывается в режиме добавления.
   2>filename
      # Перенаправление stderr в файл "filename".
   2>>filename
      # Перенаправление stderr в файл "filename", файл открывается в режиме добавления.
   &>filename
      # Перенаправление stdout и stderr в файл "filename".

      #==============================================================================
      # Перенаправление stdout, только для одной строки.
      LOGFILE=script.log

      echo "Эта строка будет записана в файл \"$LOGFILE\"." 1>$LOGFILE
      echo "Эта строка будет добавлена в конец файла \"$LOGFILE\"." 1>>$LOGFILE
      echo "Эта строка тоже будет добавлена в конец файла \"$LOGFILE\"." 1>>$LOGFILE
      echo "Эта строка будет выведена на экран и не попадет в файл \"$LOGFILE\"."
      # После каждой строки, сделанное перенаправление автоматически "сбрасывается".



      # Перенаправление stderr, только для одной строки.
      ERRORFILE=script.errors

      bad_command1 2>$ERRORFILE       #  Сообщение об ошибке запишется в $ERRORFILE.
      bad_command2 2>>$ERRORFILE      #  Сообщение об ошибке добавится в конец $ERRORFILE.
      bad_command3                    #  Сообщение об ошибке будет выведено на stderr,
                                      #+ и не попадет в $ERRORFILE.
      # После каждой строки, сделанное перенаправление также автоматически "сбрасывается".
      #==============================================================================



   2>&1
      # Перенаправляется stderr на stdout.
      # Сообщения об ошибках передаются туда же, куда и стандартный вывод.

   i>&j
      # Перенаправляется файл с дескриптором i в j.
      # Вывод в файл с дескриптором i передается в файл с дескриптором j.

   >&j
      # Перенаправляется  файл с дескриптором 1 (stdout) в файл с дескриптором j.
      # Вывод на stdout передается в файл с дескриптором j.

   0< FILENAME
    < FILENAME
      # Ввод из файла.
      # Парная команде ">", часто встречается в комбинации с ней.
      #
      # grep search-word <filename


   [j]<>filename
      # Файл "filename" открывается на чтение и запись, и связывается с дескриптором "j".
      # Если "filename" отсутствует, то он создается.
      # Если дескриптор "j" не указан, то, по-умолчанию, бередся дескриптор 0, stdin.
      #
      # Как одно из применений этого -- запись в конкретную позицию в файле.
      echo 1234567890 > File    # Записать строку в файл "File".
      exec 3<> File       # Открыть "File" и связать с дескриптором 3.
      read -n 4 <&3             # Прочитать 4 символа.
      echo -n . >&3             # Записать символ точки.
      exec 3>&-                 # Закрыть дескриптор 3.
      cat File                  # ==> 1234.67890
      # Произвольный доступ, да и только!



   |
      # Конвейер (канал).
      # Универсальное средство для объединения команд в одну цепочку.
      # Похоже на ">", но на самом деле -- более обширная.
      # Используется для объединения команд, сценариев, файлов и программ в одну цепочку (конвейер).
      cat *.txt | sort | uniq > result-file
      # Содержимое всех файлов .txt сортируется, удаляются повторяющиеся строки,
      # результат сохраняется в файле "result-file".

Операции перенаправления и/или конвейеры могут комбинироваться в одной командной строке.

command < input-file > output-file

command1 | command2 | command3 > output-file
См. Пример 12-23 и Пример A-17.

Допускается перенаправление нескольких потоков в один файл.

ls -yz >> command.log 2>&1
# Сообщение о неверной опции "yz" в команде "ls" будет записано в файл "command.log".
# Поскольку stderr перенаправлен в файл.


Закрытие дескрипторов файлов

n<&-

Закрыть дескриптор входного файла n.

0<&-, <&-

Закрыть stdin.

n>&-

Закрыть дескриптор выходного файла n.

1>&-, >&-

Закрыть stdout.

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

# В конвейер передается только stderr.

exec 3>&1                              # Сохранить текущее "состояние" stdout.
ls -l 2>&1 >&3 3>&- | grep bad 3>&-    # Закрыть дескр. 3 для 'grep' (но не для 'ls').
#              ^^^^   ^^^^
exec 3>&-                              # Теперь закрыть его для оставшейся части сценария.

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


Дополнительные сведения о перенаправлении ввода/вывода вы найдете в Приложение D.

16.1. С помощью команды exec

Команда exec <filename перенаправляет ввод со stdin на файл. С этого момента весь ввод, вместо stdin (обычно это клавиатура), будет производиться из этого файла. Это дает возможность читать содержимое файла, строку за строкой, и анализировать каждую введенную строку с помощью sed и/или awk.

Пример 16-1. Перенаправление stdin с помощью exec

#!/bin/bash
# Перенаправление stdin с помощью 'exec'.


exec 6<&0          # Связать дескр. #6 со стандартным вводом (stdin).
                   # Сохраняя stdin.

exec < data-file   # stdin заменяется файлом "data-file"

read a1            # Читается первая строка из "data-file".
read a2            # Читается вторая строка из "data-file."

echo
echo "Следующие строки были прочитаны из файла."
echo "-----------------------------------------"
echo $a1
echo $a2

echo; echo; echo

exec 0<&6 6<&-
#  Восстанавливается stdin из дескр. #6, где он был предварительно сохранен,
#+ и дескр. #6 закрывается ( 6<&- ) освобождая его для других процессов.
#
# <&6 6<&-    дает тот же результат.

echo -n "Введите строку  "
read b1  # Теперь функция "read", как и следовало ожидать, принимает данные с обычного stdin.
echo "Строка, принятая со stdin."
echo "--------------------------"
echo "b1 = $b1"

echo

exit 0

Аналогично, конструкция exec >filename перенаправляет вывод на stdout в заданный файл. После этого, весь вывод от команд, который обычно направляется на stdout, теперь выводится в этот файл.

Пример 16-2. Перенаправление stdout с помощью exec

#!/bin/bash
# reassign-stdout.sh

LOGFILE=logfile.txt

exec 6>&1           # Связать дескр. #6 со stdout.
                    # Сохраняя stdout.

exec > $LOGFILE     # stdout замещается файлом "logfile.txt".

# ----------------------------------------------------------- #
# Весь вывод от команд, в данном блоке, записывается в файл $LOGFILE.

echo -n "Logfile: "
date
echo "-------------------------------------"
echo

echo "Вывод команды \"ls -al\""
echo
ls -al
echo; echo
echo "Вывод команды \"df\""
echo
df

# ----------------------------------------------------------- #

exec 1>&6 6>&-      # Восстановить stdout и закрыть дескр. #6.

echo
echo "== stdout восстановлено в значение по-умолчанию == "
echo
ls -al
echo

exit 0

Пример 16-3. Одновременное перенаправление устройств, stdin и stdout, с помощью команды exec

#!/bin/bash
# upperconv.sh
# Преобразование символов во входном файле в верхний регистр.

E_FILE_ACCESS=70
E_WRONG_ARGS=71

if [ ! -r "$1" ]     # Файл доступен для чтения?
then
  echo "Невозможно прочитать из заданного файла!"
  echo "Порядок использования: $0 input-file output-file"
  exit $E_FILE_ACCESS
fi                   #  В случае, если входной файл ($1) не задан
                     #+ код завершения будет этим же.

if [ -z "$2" ]
then
  echo "Необходимо задать выходной файл."
  echo "Порядок использования: $0 input-file output-file"
  exit $E_WRONG_ARGS
fi


exec 4<&0
exec < $1            # Назначить ввод из входного файла.

exec 7>&1
exec > $2            # Назначить вывод в выходной файл.
                     # Предполагается, что выходной файл доступен для записи
                     # (добавить проверку?).

# -----------------------------------------------
    cat - | tr a-z A-Z   # Перевод в верхний регистр
#   ^^^^^                # Чтение со stdin.
#           ^^^^^^^^^^   # Запись в stdout.
# Однако, и stdin и stdout были перенаправлены.
# -----------------------------------------------

exec 1>&7 7>&-       # Восстановить stdout.
exec 0<&4 4<&-       # Восстановить stdin.

# После восстановления, следующая строка выводится на stdout, чего и следовало ожидать.
echo "Символы из \"$1\" преобразованы в верхний регистр, результат записан в \"$2\"."

exit 0

Примечания

[1]

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

[2]

При использрвании дескриптора с номером 5 могут возникать проблемы. Когда Bash порождает дочерний процесс, например командой exec, то дочерний процесс наследует дескриптор 5 как "открытый" (см. архив почты Чета Рамея (Chet Ramey), SUBJECT: RE: File descriptor 5 is held open) Поэтому, лучше не использовать этот дескриптор.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

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

Встроенный документ (here document) является специальной формой перенаправления ввода/вывода, которая позволяет передать список команд интерактивной программе или команде, например ftp, telnet или ex. Конец встроенного документа выделяется "строкой-ограничителем", которая задается с помощью специальной последовательности символов <<. Эта последовательность -- есть перенаправление вывода из файла в программу, напоминает конструкцию interactive-program < command-file, где command-file содержит строки:

command #1
command #2
...


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

#!/bin/bash
interactive-program <<LimitString
command #1
command #2
...
LimitString


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

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

Пример 17-1. dummyfile: Создание 2-х строчного файла-заготовки

#!/bin/bash

# Неинтерактивное редактирование файла с помощью 'vi'.
# Эмуляция 'sed'.

E_BADARGS=65

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

TARGETFILE=$1

# Вставить 2 строки в файл и сохранить.
#--------Начало встроенного документа-----------#
vi $TARGETFILE <<x23LimitStringx23
i
Это строка 1.
Это строка 2.
^[
ZZ
x23LimitStringx23
#----------Конец встроенного документа-----------#

#  Обратите внимание: ^[, выше -- это escape-символ
#+ Control-V <Esc>.

#  Bram Moolenaar указывает, что этот скрипт может не работать с 'vim',
#+ из-за возможных проблем взаимодействия с терминалом.

exit 0

Этот сценарий, с тем же эффектом, мог бы быть реализован, основываясь не на vi, а на ex. Встроенные документы, содержащие команды для ex, стали настолько обычным делом, что их уже смело можно вынести в отдельную категорию -- ex-сценарии.

Пример 17-2. broadcast: Передача сообщения всем, работающим в системе, пользователям

#!/bin/bash

wall <<zzz23EndOfMessagezzz23
Пошлите, по электронной почте, ваш заказ на пиццу, системному администратору.
    (Добавьте дополнительный доллар, если вы желаете положить на пиццу анчоусы или грибы.)
# Внимание: строки комментария тоже будут переданы команде 'wall' как часть текста.
zzz23EndOfMessagezzz23

# Возможно, более эффективно это может быть сделано так:
#         wall <message-file
# Однако, встроенный документ помогает сэкономить ваши силы и время.

exit 0

Пример 17-3. Вывод многострочных сообщений с помощью cat

#!/bin/bash

# Команда 'echo' прекрасно справляется с выводом однострочных сообщений,
# но иногда необходимо вывести несколько строк.
# Команда 'cat' и встроенный документ помогут вам в этом.

cat <<End-of-message
-------------------------------------
Это первая строка сообщения.
Это вторая строка сообщения.
Это третья строка сообщения.
Это четвертая строка сообщения.
Это последняя строка сообщения.
-------------------------------------
End-of-message

exit 0


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

# S.C. отмечает, что следующий код работает точно так же.
echo "-------------------------------------
Это первая строка сообщения.
Это вторая строка сообщения.
Это третья строка сообщения.
Это четвертая строка сообщения.
Это последняя строка сообщения.
-------------------------------------"
# Однако, в этом случае, двойные кавычки в теле сообщения, должны экранироваться.

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

Пример 17-4. Вывод многострочных сообщений с подавлением символов табуляции

#!/bin/bash
# То же, что и предыдущий сценарий, но...

#  Символ "-", начинающий строку-ограничитель встроенного документа: <<-
#  подавляет вывод символов табуляции, которые могут встречаться в теле документа,
#  но не пробелов.

cat <<-ENDOFMESSAGE
        Это первая строка сообщения.
        Это вторая строка сообщения.
        Это третья строка сообщения.
        Это четвертая строка сообщения.
        Это последняя строка сообщения.
ENDOFMESSAGE
# Текст, выводимый сценарием, будет смещен влево.
# Ведущие символы табуляции не будут выводиться.

# Вышеприведенные 5 строк текста "сообщения" начинаются с табуляции, а не с пробелов.


exit 0

Встроенные документы поддерживают подстановку команд и параметров. Что позволяет передавать различные параметры в тело встроенного документа.

Пример 17-5. Встроенные документы и подстановка параметров

#!/bin/bash
# Вывод встроенного документа командой 'cat', с использованием подстановки параметров.

# Попробуйте запустить сценарий без аргументов,   ./scriptname
# Попробуйте запустить сценарий с одним аргументом,   ./scriptname Mortimer
# Попробуйте запустить сценарий с одним аргументом, из двух слов, в кавычках,
#                           ./scriptname "Mortimer Jones"

CMDLINEPARAM=1     # Минимальное число аргументов командной строки.

if [ $# -ge $CMDLINEPARAM ]
then
  NAME=$1          # Если аргументов больше одного,
                   # то рассматривается только первый.
else
  NAME="John Doe"  # По-умолчанию, если сценарий запущен без аргументов.
fi

RESPONDENT="автора этого сценария"


cat <<Endofmessage

Привет, $NAME!
Примите поздравления от $RESPONDENT.

# Этот комментарий тоже выводится (почему?).

Endofmessage

# Обратите внимание на то, что пустые строки тоже выводятся.

exit 0

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

Пример 17-6. Отключение подстановки параметров

#!/bin/bash
# Вывод встроенного документа командой 'cat', с запретом подстановки параметров.

NAME="John Doe"
RESPONDENT="автора этого сценария"

cat <<'Endofmessage'

Привет, $NAME.
Примите поздравления от $RESPONDENT.

Endofmessage

#  Подстановка параметров не производится, если строка ограничитель
#  заключена в кавычки или экранирована.
#  Тот же эффект дают:
#  cat <<"Endofmessage"
#  cat <<\Endofmessage

exit 0

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

Пример 17-7. Передача пары файлов во входящий каталог на "Sunsite"

#!/bin/bash
# upload.sh

# Передача пары файлов (Filename.lsm, Filename.tar.gz)
# на Sunsite (ibiblio.org).

E_ARGERROR=65

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


Filename=`basename $1`           # Отсечь имя файла от пути к нему.

Server="ibiblio.org"
Directory="/incoming/Linux"
# Вообще, эти строки должны бы не "зашиваться" жестко в сценарий,
# а приниматься в виде аргумента из командной строки.

Password="your.e-mail.address"   # Измените на свой.

ftp -n $Server <<End-Of-Session
# Ключ -n запрещает автоматическую регистрацию (auto-logon)

user anonymous "$Password"
binary
bell                # "Звякнуть" после передачи каждого файла
cd $Directory
put "$Filename.lsm"
put "$Filename.tar.gz"
bye
End-Of-Session

exit 0

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

Пример 17-8. Встроенные документы и функции

#!/bin/bash
# here-function.sh

GetPersonalData ()
{
  read firstname
  read lastname
  read address
  read city
  read state
  read zipcode
} # Это немного напоминает интерактивную функцию, но...


# Передать ввод в функцию.
GetPersonalData <<RECORD001
Bozo
Bozeman
2726 Nondescript Dr.
Baltimore
MD
21226
RECORD001


echo
echo "$firstname $lastname"
echo "$address"
echo "$city, $state $zipcode"
echo

exit 0

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

Пример 17-9. "Анонимный" Встроенный Документ

#!/bin/bash

: <<TESTVARIABLES
${HOSTNAME?}${USER?}${MAIL?}  # Если одна из переменных не определена, то выводится сообщение об ошибке.
TESTVARIABLES

exit 0

Tip

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

Пример 17-10. Блочный комментарий

#!/bin/bash
# commentblock.sh

: << COMMENTBLOCK
echo "Эта строка не будет выведена."
Эта строка комментария не начинается с символа "#".
Это еще одна строка комментария, которая начинается не с символа "#".

&*@!!++=
Эта строка не вызовет ошибки,
поскольку Bash проигнорирует ее.
COMMENTBLOCK

echo "Код завершения  \"COMMENTBLOCK\" = $?."   # 0
# Показывает, что ошибок не возникало.


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

: << DEBUGXXX
for file in *
do
 cat "$file"
done
DEBUGXXX

exit 0
Tip

Еще одно остроумное применение встроенных документов -- встроенная справка к сценарию.

Пример 17-11. Встроенная справка к сценарию

#!/bin/bash
# self-document.sh: сценарий со встроенной справкой
# Модификация сценария "colm.sh".

DOC_REQUEST=70

if [ "$1" = "-h"  -o "$1" = "--help" ]     # Request help.
then
  echo; echo "Порядок использования: $0 [directory-name]"; echo
  sed --silent -e '/DOCUMENTATIONXX$/,/^DOCUMENTATION/p' "$0" |
  sed -e '/DOCUMENTATIONXX/d'; exit $DOC_REQUEST; fi

: << DOCUMENTATIONXX
Сценарий выводит сведения о заданном каталоге в виде таблице.
-------------------------------------------------------------
Сценарию необходимо передать имя каталога. Если каталог не
указан или он недоступен для чтения, то выводятся сведения
о текущем каталоге.

DOCUMENTATIONXX

if [ -z "$1" -o ! -r "$1" ]
then
  directory=.
else
  directory="$1"
fi

echo "Сведения о каталоге "$directory":"; echo
(printf "PERMISSIONS LINKS OWNER GROUP SIZE MONTH DAY HH:MM PROG-NAME\n" \
; ls -l "$directory" | sed 1d) | column -t

exit 0
Note

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

bash$ bash -c 'lsof -a -p $$ -d0' << EOF
> EOF
lsof    1213 bozo    0r   REG    3,5    0 30386 /tmp/t1213-0-sh (deleted)
             


Caution

Некоторые утилиты не могут работать внутри встроенных документов.

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


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 1. Зачем необходимо знание языка Shell?

Знание языка командной оболочки является залогом успешного решения задач администрирования системы. Даже если вы не предполагаете заниматься написанием своих сценариев. Во время загрузки Linux выполняется целый ряд сценариев из /etc/rc.d, которые настраивают конфигурацию операционной системы и запускают различные сервисы, поэтому очень важно четко понимать эти скрипты и иметь достаточно знаний, чтобы вносить в них какие либо изменения.

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

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

Скрипты возвращают нас к классической философии UNIX -- "разделяй и влавствуй" т.е. разделение сложного проекта на ряд простых подзадач. Многие считают такой подход наилучшим или, по меньшей мере, наиболее эстетичным способом решения возникающих проблем, нежели использование нового поколения языков -- "все-в-одном", таких как Perl.

Для каких задач неприменимы скрипты



Если выполняется хотя бы одно из вышеперечисленных условий, то вам лучше обратиться к более мощным скриптовым языкам программирования, например Perl, Tcl, Python, Ruby или к высокоуровневым компилирующим языкам -- C, C++ или Java. Но даже в этом случае, создание прототипа приложения на языке shell может существенно облегчить разработку.

Название BASH -- это аббревиатура от "Bourne-Again Shell" и игра слов от, ставшего уже классикой, "Bourne Shell" Стефена Бурна (Stephen Bourne). В последние годы BASH достиг такой популярности, что стал стандартной командной оболочкой de facto для многих разновидностей UNIX. Большинство принципов программирования на BASH одинаково хорошо применимы и в других командных оболочках, таких как Korn Shell (ksh), от которой Bash позаимствовал некоторые особенности, [2] и C Shell и его производных. (Примечательно, что C Shell не рекомендуется к использованию из-за отдельных проблем, отмеченных Томом Кристиансеном (Tom Christiansen) в октябре 1993 года на Usenet post

Далее, в тексте документа вы найдете большое количество примеров скриптов, иллюстрирующих возможности shell. Все примеры -- работающие. Они были протестированы, причем некоторые из них могут пригодиться в повседневной работе. Уважаемый читатель можеть "поиграть" с рабочим кодом скриптов, сохраняя их в файлы, с именами scriptname.sh. [3] Не забудьте выдать этим файлам право на исполнение (chmod u+rx scriptname), после чего сценарии можно будет запустить на исполнение и проверить результат их работы. Вам следует помнить, что описание некоторых примеров следует после исходного кода этого примера, поэтому, прежде чем запустить сценарий у себя -- ознакомьтесь с его описанием.

Скрипты были написаны автором книги, если не оговаривается иное.

Примечания

[1]

Их так же называют встроенными конструкциями языка командной оболочки shell.

[2]

Многие особенности ksh88 и даже ksh93 перекочевали в Bash.

[3]

В соответствии с соглашениями, имена файлов с shell-скриптами, такими как Bourne shell и совместимыми, имеют расширение .sh. Все стартовые скрипты, которые вы найдете в /etc/rc.d, следуют этому соглашению.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 18. Регулярные выражения

Для того, чтобы полностью реализовать потенциал командной оболочки, вам придется овладеть Регулярными Выражениями. Многие команды и утилиты, обычно используемые в сценариях, такие как grep, expr, sed и awk, используют Регулярные Выражения.

18.1. Краткое введение в регулярные выражения

Выражение -- это строка символов. Символы, которые имеют особое назначение, называются метасимволами. Так, например, кавычки могут выделять прямую речь, т.е. быть метасимволами для строки, заключенной в эти кавычки. Регулярные выражения -- это набор символов и/или метасимволов, которые наделены особыми свойствами. [1]

Основное назначение регулярных выражений -- это поиск текста по шаблону и работа со строками.

Note

Некоторые версии sed, ed и ex поддерживают экранированные версии регулярных выражений, описанных выше.

Sed, awk и Perl, используемые в сценариях в качестве фильтров, могут принимать регулярные выражения в качестве входных аргументов. См. Пример A-13 и Пример A-19.

Книга "Sed & Awk" (авторы Dougherty и Robbins) дает полное и ясное представление о регулярных выражениях (см. раздел Литература).

Примечания

[1]

В качестве простейшего регулярного выражения можно привести строку, не содержащую никаких метасимволов.

[2]

Поскольку с помощью sed, awk и grep обрабатывают одиночные строки, то обычно символ перевода строки не принимается во внимание. В тех же случаях, когда производится разбор многострочного текста, метасимвол "точка" будет соответствовать символу перевода строки.

#!/bin/bash

sed -e 'N;s/.*/[&]/' << EOF   # Встроенный документ
line1
line2
EOF
# OUTPUT:
# [line1
# line2]



echo

awk '{ $0=$1 "\n" $2; if (/line.1/) {print}}' << EOF
line 1
line 2
EOF
# OUTPUT:
# line
# 1


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

exit 0



Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 19. Подоболочки, или Subshells

Запуск сценария приводит к запуску дочернего командного интерпретатора. Который выполняет интерпретацию и исполнение списка команд, содержащихся в файле сценария, точно так же, как если бы они были введены из командной строки. Любой сценарий запускается как дочерний процесс родительской командной оболочки, той самой, которая выводит перед вами строку приглашения к вводу на консоли или в окне xterm.

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

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

( command1; command2; command3; ... )

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

Note

Значения переменных, определенных в дочерней оболочке, не могут быть переданы родительской оболочке. Они недоступны родительскому процессу. Фактически, они ведут себя как локальные переменные.

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

#!/bin/bash
# subshell.sh

echo

outer_variable=Outer

(
inner_variable=Inner
echo "Дочерний процесс, \"inner_variable\" = $inner_variable"
echo "Дочерний процесс, \"outer\" = $outer_variable"
)

echo

if [ -z "$inner_variable" ]
then
  echo "Переменная inner_variable не определена в родительской оболочке"
else
  echo "Переменная inner_variable определена в родительской оболочке"
fi

echo "Родительский процесс, \"inner_variable\" = $inner_variable"
# Переменная $inner_variable не будет определена
# потому, что переменные, определенные в дочернем процессе,
# ведут себя как "локальные переменные".

echo

exit 0

См. также Пример 31-1.

+

Смена текущего каталога в дочернем процессе (подоболочке) не влечет за собой смену текущего каталога в родительской оболочке.

Пример 19-2. Личные настройки пользователей

#!/bin/bash
# allprofs.sh: вывод личных настроек (profiles) всех пользователей

# Автор: Heiner Steven
# С некоторыми изменениями, внесенными автором документа.

FILE=.bashrc  #  Файл настроек пользователя,
              #+ в оригинальном сценарии называется ".profile".

for home in `awk -F: '{print $6}' /etc/passwd`
do
  [ -d "$home" ] || continue    # Перейти к следующей итерации, если нет домашнего каталога.
  [ -r "$home" ] || continue    # Перейти к следующей итерации, если не доступен для чтения.
  (cd $home; [ -e $FILE ] && less $FILE)
done

#  По завершении сценария -- нет теобходимости выполнять команду 'cd', чтобы вернуться в первоначальный каталог,
#+ поскольку 'cd $home' выполняется в подоболочке.

exit 0

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

COMMAND1
COMMAND2
COMMAND3
(
  IFS=:
  PATH=/bin
  unset TERMINFO
  set -C
  shift 5
  COMMAND4
  COMMAND5
  exit 3 # Выход только из подоболочки.
)
# Изменение переменных окружения не коснется родительской оболочки.
COMMAND6
COMMAND7
Как вариант использования подоболочки -- проверка переменных.
if (set -u; : $variable) 2> /dev/null
then
  echo "Переменная определена."
fi

# Можно сделать то же самое по другому: [[ ${variable-x} != x || ${variable-y} != y ]]
# или                                   [[ ${variable-x} != x$variable ]]
# или                                   [[ ${variable+x} = x ]])
Еще одно применение -- проверка файлов блокировки:
if (set -C; : > lock_file) 2> /dev/null
then
  echo "Этот сценарий уже запущен другим пользователем."
  exit 65
fi

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


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

Пример 19-3. Запуск нескольких процессов в подоболочках

       (cat list1 list2 list3 | sort | uniq > list123) &
        (cat list4 list5 list6 | sort | uniq > list456) &
        # Слияние и сортировка двух списков производится одновременно.
        # Запуск в фоне гарантирует параллельное исполнение.
        #
        # Тот же эффект дает
        #   cat list1 list2 list3 | sort | uniq > list123 &
        #   cat list4 list5 list6 | sort | uniq > list456 &

        wait   # Ожидание завершения работы подоболочек.

        diff list123 list456

Перенаправление ввода/вывода в/из подоболочки производится оператором построения конвейера "|", например, ls -al | (command).

Note

Блок команд, заключенный в фигурные скобки не приводит к запуску дочерней подоболочки.

{ command1; command2; command3; ... }


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 20. Ограниченный режим командной оболочки

Команды, запрещенные в ограниченном режиме командной оболочки

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

В ограниченном режиме запрещена команда cd -- смена текщего каталога.

Запрещено изменять переменные окружения $PATH, $SHELL, $BASH_ENV и $ENV.

Заперщен доступ к переменной $SHELLOPTS.

Запрещено перенаправление вывода.

Запрещен вызов утилит, в названии которых присутствует хотя бы один символ "слэш" (/).

Запрещен вызов команды exec для запуска другого процесса.

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

Запрещен выход из ограниченного режима.

Пример 20-1. Запуск сценария в ограниченном режиме

#!/bin/bash
# Если sha-bang задать в таком виде: "#!/bin/bash -r"
# то это приведет к включению ограниченного режима с момента запуска скрипта.

echo

echo "Смена каталога."
cd /usr/local
echo "Текущий каталог: `pwd`"
echo "Переход в домашний каталог."
cd
echo "Текущий каталог: `pwd`"
echo

# До сих пор сценарий исполнялся в обычном, неограниченном режиме.

set -r
# set --restricted    имеет тот же эффект.
echo "==> Переход в ограниченный режим. <=="

echo
echo

echo "Попытка сменить текущий каталог в ограниченном режиме."
cd ..
echo "Текущий каталог остался прежним: `pwd`"

echo
echo

echo "\$SHELL = $SHELL"
echo "Попытка смены командного интерпретатора в ограниченном режиме."
SHELL="/bin/ash"
echo
echo "\$SHELL= $SHELL"

echo
echo

echo "Попытка перенаправления вывода в ограниченном режиме."
ls -l /usr/bin > bin.files
ls -l bin.files    # Попробуем найти файл, который пытались создать.

echo

exit 0

Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 21. Подстановка процессов

Подстановка процессов -- это аналог подстановки команд. Операция подстановки команд записывает в переменную результат выполнения некоторой команды, например, dir_contents=`ls -al` или xref=$(grep word datafile). Операция подстановки процессов передает вывод одного процесса на ввод другого (другими словами, передает результат выполнения одной команды -- другой).

Шаблон подстановки команды

Внутри круглых скобок

>(command)

<(command)

Таким образом инициируется подстановка процессов. Здесь, для передачи результата работы процесса в круглых скобках, используются файлы /dev/fd/<n>. [1]

Note

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

bash$ echo >(true)
/dev/fd/63

bash$ echo <(true)
/dev/fd/63
             
Bash создает канал с двумя файловыми дескрипторами, --fIn и fOut--. stdin команды true присоединяется к fOut (dup2(fOut, 0)), затем Bash передает /dev/fd/fIn в качестве аргумента команде echo. В системах, где отсутствуют файлы /dev/fd/<n>, Bash может использовать временные файлы. (Спасибо S.C.)

cat <(ls -l)
# То же самое, что и     ls -l | cat

sort -k 9 <(ls -l /bin) <(ls -l /usr/bin) <(ls -l /usr/X11R6/bin)
# Список файлов в трех основных каталогах 'bin', отсортированный по именам файлов.
# Обратите внимание: на вход 'sort' поданы три самостоятельные команды.


diff <(command1) <(command2)    # Выдаст различия в выводе команд.

tar cf >(bzip2 -c > file.tar.bz2) $directory_name
# Вызовет "tar cf /dev/fd/?? $directory_name" и затем "bzip2 -c > file.tar.bz2".
#
# Из-за особенностей, присущих некоторым системам, связанным с /dev/fd/<n>,
# канал между командами не обязательно должен быть именованным.
#
# Это можно сделать и так.
#
bzip2 -c < pipe > file.tar.bz2&
tar cf pipe $directory_name
rm pipe
#        или
exec 3>&1
tar cf /dev/fd/4 $directory_name 4>&1 >&3 3>&- | bzip2 -c > file.tar.bz2 3>&-
exec 3>&-


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


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

# Фрагмент сценария из дистрибутива SuSE:

while read  des what mask iface; do
# Некоторые команды ...
done < <(route -n)


# Чтобы проверить это, попробуем вставить команду, выполняющую какие либо действия.
while read  des what mask iface; do
  echo $des $what $mask $iface
done < <(route -n)

# Вывод на экран:
# Kernel IP routing table
# Destination Gateway Genmask Flags Metric Ref Use Iface
# 127.0.0.0 0.0.0.0 255.0.0.0 U 0 0 0 lo


# Как указывает S.C. -- более простой для понимания эквивалент:
route -n |
  while read des what mask iface; do   # Переменные берут значения с устройства вывода конвейера (канала).
    echo $des $what $mask $iface
  done  #  На экран выводится то же самое, что и выше.
        #  Однако, Ulrich Gayer отметил, что ...
        #+ этот вариант запускает цикл while в подоболочке,
        #+ и поэтому переменные не видны за пределами цикла, после закрытия канала.


Примечания

[1]

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


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 22. Функции

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

function function_name {
command...
}

или

function_name () {
command...
}



Вторая форма записи ближе к сердцу C-программистам (она же более переносимая).

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

function_name ()
{
command...
}



Вызов функции осуществляется простым указанием ее имени в тексте сценария.

Пример 22-1. Простая функция

#!/bin/bash

funky ()
{
  echo "Это обычная функция."
} # Функция должна быть объявлена раньше, чем ее можно будет использовать.

  # Вызов функции.

funky

exit 0

Функция должна быть объявлена раньше, чем ее можно будет использовать. К сожалению, в Bash нет возможности "опережающего объявления" функции, как например в C.

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

declare -f f1      # Это не поможет.
f1                 # По прежнему -- сообщение об ошибке.

# Однако...


f1 ()
{
  echo "Вызов функции \"f2\" из функции \"f1\"."
  f2
}

f2 ()
{
  echo "Функция \"f2\"."
}

f1  #  Функция "f2", фактически, не вызывается выше этой строки,
    #+ хотя ссылка на нее встречается выше, до ее объявления.
    #  Это допускается.

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


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

f1 ()
{

  f2 () # вложенная
  {
    echo "Функция \"f2\", вложенная в \"f1\"."
  }

}

f2  #  Вызывает сообщение об ошибке.
    #  Даже "declare -f f2" не поможет.

echo

f1  #  Ничего не происходит, простой вызов "f1", не означает автоматический вызов "f2".
f2  #  Теперь все нормально, вызов "f2" не приводит к появлению ошибки,
    #+ поскольку функция "f2" была определена в процессе вызова "f1".

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


Объявление функции может размещаться в самых неожиданных местах.

ls -l | foo() { echo "foo"; }  # Допустимо, но бесполезно.



if [ "$USER" = bozo ]
then
  bozo_greet ()   # Объявление функции размещено в условном операторе.
  {
    echo "Привет, Bozo!"
  }
fi

bozo_greet        # Работает только у пользователя bozo, другие получат сообщение об ошибке.



# Нечто подобное можно использовать с определеной пользой для себя.
NO_EXIT=1   # Will enable function definition below.

[[ $NO_EXIT -eq 1 ]] && exit() { true; }     # Определение функции в последовательности "И-список".
# Если $NO_EXIT равна 1, то объявляется "exit ()".
# Тем самым, функция "exit" подменяет встроенную команду "exit".

exit  # Вызывается функция "exit ()", а не встроенная команда "exit".

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


22.1. Сложные функции и сложности с функциями

Функции могут принимать входные аргументы и возвращать код завершения.

function_name $arg1 $arg2

Доступ к входным аргументам, в функциях, производится посредством позиционных параметров, т.е. $1, $2 и так далее.

Пример 22-2. Функция с аргументами

#!/bin/bash
# Функции и аргументы

DEFAULT=default                             # Значение аргумента по-умолчанию.

func2 () {
   if [ -z "$1" ]                           # Длина аргумента #1 равна нулю?
   then
     echo "-Аргумент #1 имеет нулевую длину.-"  # Или аргумент не был передан функции.
   else
     echo "-Аргумент #1: \"$1\".-"
   fi

   variable=${1-$DEFAULT}                   #  Что делает
   echo "variable = $variable"              #+ показанная подстановка параметра?
                                            #  ---------------------------
                                            #  Она различает отсутствующий аргумент
                                            #+ от "пустого" аргумента.

   if [ "$2" ]
   then
     echo "-Аргумент #2: \"$2\".-"
   fi

   return 0
}

echo

echo "Вызов функции без аргументов."
func2
echo


echo "Вызов функции с \"пустым\" аргументом."
func2 ""
echo

echo "Вызов функции с неинициализированным аргументом."
func2 "$uninitialized_param"
echo

echo "Вызов функции с одним аргументом."
func2 first
echo

echo "Вызов функции с двумя аргументами."
func2 first second
echo

echo "Вызов функции с аргументами \"\" \"second\"."
func2 "" second       # Первый параметр "пустой"
echo                  # и второй параметр -- ASCII-строка.

exit 0
Important

Команда shift вполне применима и к аргументам функций (см. Пример 33-10).

Note

В отличие от других языков программирования, в сценариях на языке командной оболочке, в функции передаются аргументы по значению. [1] Если имена переменных (которые фактически являются указателями) передаются функции в виде аргументов, то они интерпретируются как обычные строки символов и не могут быть разыменованы. Функции интерпретируют свои аргументы буквально.

Exit и Return

код завершения

Функции возвращают значение в виде кода завершения. Код завершения может быть задан явно, с помощью команды return, в противном случае будет возвращен код завершения последней команды в функции (0 -- в случае успеха, иначе -- ненулевой код ошибки). Код завершения в сценарии может быть получен через переменную $?.

return

Завершает исполнение функции. Команда return [2] может иметь необязательный аргумент типа integer, который возвращается в вызывающий сценарий как "код завершения" функции, это значение так же записывается в переменную $?.

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

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

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

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

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

max2 33 34
return_val=$?

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


exit 0

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

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

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

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

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


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

#!/bin/bash

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

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

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

LIMIT=200
E_ARG_ERR=65
E_OUT_OF_RANGE=66

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

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

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

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


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

echo

exit 0

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

Important

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

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

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

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

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

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

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

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

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

exit 0

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

return $retval
}

max2 33001 33997
return_val=$?

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


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

exit 0

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

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

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

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

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

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

#!/bin/bash

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

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

file=/etc/passwd
pattern=$1

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

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

file_excerpt $pattern

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

exit 0

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

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

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

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

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

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


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


Примечания

[1]

Механизм косвенных ссылок на переменные (см. Пример 34-2) слишком неудобен для передачи аргументов по ссылке.

#!/bin/bash

ITERATIONS=3  # Количество вводимых значений.
icount=1

my_read () {
  # При вызове my_read varname,
  # выводит предыдущее значение в квадратных скобках,
  # затем просит ввести новое значение.

  local local_var

  echo -n "Введите говое значение переменной "
  eval 'echo -n "[$'$1'] "'  # Прежнее значение.
  read local_var
  [ -n "$local_var" ] && eval $1=\$local_var

  # Последовательность "And-list": если "local_var" не пуста, то ее значение переписывается в "$1".
}

echo

while [ "$icount" -le "$ITERATIONS" ]
do
  my_read var
  echo "Значение #$icount = $var"
  let "icount += 1"
  echo
done


# Спасибо Stephane Chazelas за этот поучительный пример.

exit 0


[2]

Команда return -- это встроенная команда Bash.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

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

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

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

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

#!/bin/bash

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

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

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

echo

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

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

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




TRUE=1

echo

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

echo

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

echo; echo

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

exit 0

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

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

#!/bin/bash

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

alias llm='ls -al | more'
llm

echo

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

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

./unalias.sh: llm: command not found

Примечания

[1]

Однако, псевдонимы могут "раскручивать" позиционные параметры.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

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

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

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

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

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

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

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


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


exit 0

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

#!/bin/bash

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

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

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

exit 0

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

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

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

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


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

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

#!/bin/bash

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

E_BADARGS=65

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


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

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

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

exit 0
Important

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

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

false && true || echo false         # false

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

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

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

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


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


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

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

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

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

#!/bin/bash


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

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

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


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

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

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

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

echo

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

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

echo; echo; echo

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

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

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

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

echo; echo; echo

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

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

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

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

exit 0
Note

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

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

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

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

#!/bin/bash
# poem.sh

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

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

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

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

exit 0

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

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

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

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



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

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


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

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

#!/bin/bash

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

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

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

echo

element_count=${#colors[@]}

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

index=0

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

echo

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

echo

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

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

exit 0

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

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

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

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


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

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

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

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

ListArray

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

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

ListArray

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

ListArray

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

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

ListArray

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

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

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

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

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

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

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

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

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

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

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

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

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

exit 0

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

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

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

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


Tip

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

#!/bin/bash

filename=sample_file

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


declare -a array1

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

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

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


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

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

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


CpArray_Mac() {

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

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

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

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

Hype()
{

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

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

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

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

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

Hype before after

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

# Еще?

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

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

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

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

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

exit 0

--

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

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

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

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


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

  return
}

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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

exit 0

--

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

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

# Автор: Michael Zick.

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

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

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

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

exit 0


--

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

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

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

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


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

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

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


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


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

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

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

print
echo

exit 0

--

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

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

#!/bin/bash
# sieve.sh

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

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

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

PRIME=1
NON_PRIME=0

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


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

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

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

i=$LOWER_LIMIT

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

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

  let "i += 1"

done

}

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

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

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

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

  t=$i

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

fi

  let "i += 1"
done


}


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

echo

exit 0



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

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

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

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

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

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

exit 0

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

--

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

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

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

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

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

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

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

declare -a stack


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

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

return
}

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

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

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

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


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

echo

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

echo

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

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

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

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

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


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

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

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

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

exit 0

--

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

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

#!/bin/bash

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

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

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

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


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

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

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

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

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

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

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

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

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

done

echo

exit 0

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

--

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

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

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

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

Rows=5
Columns=5

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

load_alpha ()
{
local rc=0
local index


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

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

print_alpha ()
{
local row=0
local index

echo

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

  local column=0

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

  let "row += 1"
  echo

done

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

echo
}

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

echo -n "  "

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

}

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

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

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

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

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

  echo; echo

done

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

}


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


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

exit 0

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

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


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

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

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

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

/etc/profile

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

/etc/bashrc

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

$HOME/.bash_profile

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

$HOME/.bashrc

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

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

$HOME/.bash_logout

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

Примечания

[1]

Это не относится к таким оболочкам, как csh, tcsh и другим, которые не являются производными от классической Bourne shell (sh).


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

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

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

27.1. /dev

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

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


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

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

Примечания

[1]

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

Некоторые из устройств, такие как /dev/null, /dev/zero или /dev/urandom -- являются виртуальными. Они не являются файлами физических устройств, система эмулирует эти устройства программным способом.

[2]

Блочное устройство читает и/или пишет данные целыми блоками, в отличие от символьных устройств, которые читают и/или пишут данные по одному символу. Примером блочного устройства может служить жесткий диск, CD-ROM. Примером символьного устройства -- клавиатура.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

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

/dev/null

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

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

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


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

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


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

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


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

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

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


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

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

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

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

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

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

#!/bin/bash

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

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

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

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


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


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


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

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

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

exit $SUCCESS

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

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

#!/bin/bash
# ramdisk.sh

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


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

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

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

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

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

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

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

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

exit 0

Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

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

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

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

#!/bin/bash
# ex74.sh

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

a=37

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

exit 0

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

./ex74.sh: [37: command not found
Что в этом сценарии может быть неправильно (подсказка: после ключевого слова if)?

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

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

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

exit 0

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

missing-keyword.sh: line 11: syntax error: unexpected end of file
       
Обратите внимание, сообщение об ошибке будет содержать номер не той строки, в которой возникла ошибка, а той, в которой Bash точно установил наличие ошибочной ситуации.

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

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

Пример 29-3. test24

#!/bin/bash

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


badname=`ls | grep ' '`

# echo "$badname"

rm "$badname"

exit 0

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

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

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


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

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

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

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



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

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

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

  3. ключи -n -v -x

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

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

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

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

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

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

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

    #!/bin/bash
    # assert.sh
    
    assert ()                 #  Если условие ложно,
    {                         #+ выход из сценария с сообщением об ошибке.
      E_PARAM_ERR=98
      E_ASSERT_FAILED=99
    
    
      if [ -z "$2" ]          # Недостаточное количество входных параметров.
      then
        return $E_PARAM_ERR
      fi
    
      lineno=$2
    
      if [ ! $1 ]
      then
        echo "Утверждение ложно:  \"$1\""
        echo "Файл: \"$0\", строка: $lineno"
        exit $E_ASSERT_FAILED
      # else
      #   return
      #   и продолжить исполнение сценария.
      fi
    }
    
    
    a=5
    b=4
    condition="$a -lt $b"     # Сообщение об ощибке и завершение сценария.
                              #  Попробуйте поменять условие "condition"
                              #+ на что нибудь другое и
                              #+ посмотреть -- что получится.
    
    assert "$condition" $LINENO
    # Сценарий продолжит работу только в том случае, если утверждение истинно.
    
    
    # Прочие команды.
    # ...
    echo "Эта строка появится на экране только если утверждение истинно."
    # ...
    # Прочие команды.
    # ...
    
    exit 0
    
  5. Ловушка на выхто в этом сценарии может быть неправильно (подсказка: после ключевого словоде.

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



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

trap

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

Note

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

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

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


Пример 29-5. Ловушка на выходе

#!/bin/bash

trap 'echo Список переменных --- a = $a  b = $b' EXIT
# EXIT -- это название сигнала, генерируемого при выходе из сценария.

a=39

b=36

exit 0
# Примечательно, что если закомментировать команду 'exit',
# то это никак не скажется на работе сценария,
# поскольку "выход" из сценария происходит в любом случае.

Пример 29-6. Удаление временного файла при нажатии на Control-C

#!/bin/bash
# logon.sh: Сценарий, написаный "на скорую руку", контролирует вход в режим on-line.


TRUE=1
LOGFILE=/var/log/messages
# Обратите внимание: $LOGFILE должен быть доступен на чтение (chmod 644 /var/log/messages).
TEMPFILE=temp.$$
# "Уникальное" имя для временного файла, где расширение в имени -- это pid процесса-сценария.
KEYWORD=address
# При входе, в файл /var/log/messages,
# добавляется  строка "remote IP address xxx.xxx.xxx.xxx"
ONLINE=22
USER_INTERRUPT=13
CHECK_LINES=100
# Количество проверяемых строк.

trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT
# Удалить временный файл, когда сценарий завершает работу по control-c.

echo

while [ $TRUE ]  #Бесконечный цикл.
do
  tail -$CHECK_LINES $LOGFILE> $TEMPFILE
  # Последние 100 строк из системного журнала переписать во временный файл.
  # Совершенно необходимо, т.к. новейшие версии ядер генерируют много сообщений при входе.
  search=`grep $KEYWORD $TEMPFILE`
  # Проверить наличие фразы "address",
  # свидетельствующей об успешном входе.

  if [ ! -z "$search" ] # Кавычки необходимы, т.к. переменная может содержать пробелы.
  then
     echo "On-line"
     rm -f $TEMPFILE    # Удалить временный файл.
     exit $ONLINE
  else
     echo -n "."        # ключ -n подавляет вывод символа перевода строки,
                        # так вы получите непрерывную строку точек.
  fi

  sleep 1
done


# Обратите внимание: если изменить содержимое переменной KEYWORD
# на "Exit", то сценарий может использоваться для контроля
# неожиданного выхода (logoff).

exit 0

# Nick Drage предложил альтернативный метод:

while true
  do ifconfig ppp0 | grep UP 1> /dev/null && echo "соединение установлено" && exit 0
  echo -n "."   # Печать последовательности точек (.....), пока соединение не будет установлено.
  sleep 2
done

# Проблема: Нажатия Control-C может оказаться недостаточным, чтобы завершить этот процесс.
#          (Точки продолжают выводиться на экран.)
# Упражнение: Исправьте этот недостаток.



# Stephane Chazelas предложил еще одну альтернативу:

CHECK_INTERVAL=1

while ! tail -1 "$LOGFILE" | grep -q "$KEYWORD"
do echo -n .
   sleep $CHECK_INTERVAL
done
echo "On-line"

# Упражнение: Найдите сильные и слабые стороны
#           каждого из этих подходов.
Note

Аргумент DEBUG, команды trap, заставляет сценарий выполнять указанное действие после выполнения каждой команды. Это можно использовать для трассировки переменных.

Пример 29-7. Трассировка переменной

#!/bin/bash

trap 'echo "VARIABLE-TRACE> $LINENO: \$variable = \"$variable\""' DEBUG
# Выводить значение переменной после исполнения каждой команды.

variable=29

echo "Переменная \"\$variable\" инициализирована числом $variable."

let "variable *= 3"
echo "Значение переменной \"\$variable\" увеличено в 3 раза."

# Конструкция "trap 'commands' DEBUG" может оказаться очень полезной
# при отладке больших и сложных скриптов,
# когда размещение множества инструкций "echo $variable"
# может потребовать достаточно большого времени.

# Спасибо Stephane Chazelas.

exit 0


Note

Конструкция trap '' SIGNAL (две одиночных кавычки) -- запрещает SIGNAL для оставшейся части сценария. Конструкция trap SIGNAL -- восстанавливает действие сигнала SIGNAL. Эти конструкции могут использоваться для защиты критических участков сценария от нежелательного прерывания.

       trap '' 2  # Сигнал 2 (Control-C) -- запрещен.
        command
        command
        command
        trap 2     # Разрешение реакции на Control-C
       


Примечания

[1]

Bash debugger (автор: Rocky Bernstein) частично возмещает этот недостаток.

[2]

В соответствии с соглашениями, сигнал с номером 0 соответствует команде exit.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 30. Необязательные параметры (ключи)

Необязательные параметры -- это дополнительные ключи (опции), которые оказывают влияние на поведение сценария и/или командной оболочки.

Команда set позволяет задавать дополнительные опции прямо внутри сценария. В том месте сценария, где необходимо, чтобы та или иная опция вступила в силу, вставьте такую конструкцию set -o option-name, или в более короткой форме -- set -option-abbrev. Эти две формы записи совершенно идентичны по своему действию.

      #!/bin/bash

      set -o verbose
      # Вывод команд перед их исполнением.
     


      #!/bin/bash

      set -v
      # Имеет тот же эффект, что и выше.
     


Note

Для того, чтобы отключить действие той или иной опции, следует вставить конструкцию set +o option-name, или set +option-abbrev.

      #!/bin/bash

      set -o verbose
      # Вывод команд перед их исполнением.
      command
      ...
      command

      set +o verbose
      # Запретить вывод команд перед их исполнением.
      command
      # команда не выводится.


      set -v
      # Вывод команд перед их исполнением.
      command
      ...
      command

      set +v
      # Запретить вывод команд перед их исполнением.
      command

      exit 0
     


Как вариант установки опций, можно предложить указывать их в заголовке сценария (в строке sha-bang) -- #!.

      #!/bin/bash -x
      #
      # Далее следует текст сценария.
     


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

bash -v script-name

bash -o verbose script-name

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

Таблица 30-1. Ключи Bash

Краткое имя Полное имя Описание
-C noclobber Предотвращает перезапись файла в операциях перенаправления вывода (не распространяется на конвейеры (каналы) -- >|)
-D (нет) Выводит список строк в двойных кавычках, которым предшествует символ $, сам сценарий не исполняется
-a allexport Экспорт всех, определенных в сценарии, переменных
-b notify Выводит уведомление по завершении фоновой задачи (job) (довольно редко используется в сценариях)
-c ... (нет) Читает команды из ...
-f noglob Подстановка имен файлов (globbing) запрещена
-i interactive Сценарий запускается в интерактивном режиме
-p privileged Сценарий запускается как "suid" (осторожно!)
-r restricted Сценарий запускается в ограниченном режиме (см. Глава 20).
-u nounset При попытке обращения к неопределенным переменным, выдает сообщение об ошибке и прерывает работу сценария
-v verbose Выводит на stdout каждую команду прежде, чем она будет исполнена
-x xtrace Подобна -v, но выполняет подстановку команд
-e errexit Прерывает работу сценария при появлении первой же ошибки (когда команда возвращает ненулевой код завершения)
-n noexec Читает команды из сценария, но не исполняет их (проверка синтаксиса)
-s stdin Читает команды с устройства stdin
-t (нет) Выход после исполнения первой команды
- (нет) Конец списка ключей (опций), последующие аргументы будут восприниматься как позиционные параметры.
-- (нет) Эквивалент предыдущей опции (-).

Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 31. Широко распространенные ошибки

 

Turandot: Gli enigmi sono tre, la morte una!

Caleph: No, no! Gli enigmi sono tre, una la vita!

  Puccini

Использование зарезервированных слов и служебных символов в качестве имен переменных.

case=value0       # Может вызвать проблемы.
23skidoo=value1   # Тоже самое.
# Имена переменных, начинающиеся с цифр, зарезервированы командной оболочкой.
# Если имя переменной начинается с символа подчеркивания: _23skidoo=value1, то это не считается ошибкой.

# Однако... если имя переменной состоит из единственного символа подчеркивания, то это ошибка.
_=25
echo $_           # $_  -- это внутренняя переменная.

xyz((!*=value2    # Вызывает серьезные проблемы.


Использование дефиса, и других зарезервированных символов, в именах переменных.

var-1=23
# Вместо такой записи используйте 'var_1'.


Использование одинаковых имен для переменных и функций. Это делает сценарий трудным для понимания.

do_something ()
{
  echo "Эта функция должна что-нибудь сделать с \"$1\"."
}

do_something=do_something

do_something do_something

# Все это будет работать правильно, но слишком уж запутанно.


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

var1 = 23   # Правильный вариант: 'var1=23'.
# В вышеприведенной строке Bash будет трактовать "var1" как имя команды
# с аргументами "=" и "23".

let c = $a - $b   # Правильный вариант: 'let c=$a-$b' или 'let "c = $a - $b"'

if [ $a -le 5]    # Правильный вариант: if [ $a -le 5 ]
# if [ "$a" -le 5 ]   еще лучше.
# [[ $a -le 5 ]] тоже верно.


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

#!/bin/bash

echo "uninitialized_var = $uninitialized_var"
# uninitialized_var =


Часто программисты путают операторы сравнения = и -eq. Запомните, оператор = используется для сравнения строковых переменных, а -eq -- для сравнения целых чисел.

if [ "$a" = 273 ]      # Как вы полагаете? $a -- это целое число или строка?
if [ "$a" -eq 273 ]    # Если $a -- целое число.

# Иногда, такого рода ошибка никак себя не проявляет.
# Однако...


a=273.0   # Не целое число.

if [ "$a" = 273 ]
then
  echo "Равны."
else
  echo "Не равны."
fi    # Не равны.

# тоже самое и для  a=" 273"  и  a="0273".


# Подобные проблемы возникают при использовании "-eq" со строковыми значениями.

if [ "$a" -eq 273.0 ]
then
  echo "a = $a'
fi  # Исполнение сценария прерывается по ошибке.
# test.sh: [: 273.0: integer expression expected


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

#!/bin/bash
# bad-op.sh

number=1

while [ "$number" < 5 ]    # Неверно! должно быть   while [ "number" -lt 5 ]
do
  echo -n "$number "
  let "number += 1"
done

# Этот сценарий генерирует сообщение об ошибке:
# bad-op.sh: 5: No such file or directory


Иногда, в операциях проверки, с использованием квадратных скобок ([ ]), переменные необходимо брать в двойные кавычки. См. Пример 7-6, Пример 16-4 и Пример 9-6.

Иногда сценарий не в состоянии выполнить команду из-за нехватки прав доступа. Если пользователь не сможет запустить команду из командной строки, то эта команда не сможет быть запущена и из сценария. Попробуйте изменить атрибуты команды, возможно вам придется установить бит suid.

Использование символа - в качестве оператора перенаправления (каковым он не является) может приводить к неожиданным результатам.

command1 2> - | command2  # Попытка передать сообщения об ошибках команде command1 через конвейер...
#    ...не будет работать.

command1 2>& - | command2  # Так же бессмысленно.

Спасибо S.C.


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

#!/bin/bash

minimum_version=2
# Поскольку Chet Ramey постоянно развивает Bash,
# вам может потребоваться указать другую минимально допустимую версию $minimum_version=2.XX.
E_BAD_VERSION=80

if [ "$BASH_VERSION" \< "$minimum_version" ]
then
  echo "Этот сценарий должен исполняться под управлением Bash, версии $minimum или выше."
  echo "Настоятельно рекомендуется обновиться."
  exit $E_BAD_VERSION
fi

...


Использование специфических особенностей Bash может приводить к аварийному завершению сценария в Bourne shell (#!/bin/sh). Как правило, в Linux дистрибутивах, sh является псевдонимом bash, но это не всегда верно для UNIX-систем вообще.

Сценарий, в котором строки отделяются друг от друга в стиле MS-DOS (\r\n), будет завершаться аварийно, поскольку комбинация #!/bin/bash\r\n считается недопустимой. Исправить эту ошибку можно простым удалением символа \r из сценария.

#!/bin/bash

echo "Начало"

unix2dos $0    # Сценарий переводит символы перевода строки в формат DOS.
chmod 755 $0   # Восстановление прав на запуск.
               # Команда 'unix2dos' удалит право на запуск из атрибутов файла.

./$0           # Попытка запустить себя самого.
               # Но это не сработает из-за того, что теперь строки отделяются
               # друг от друга в стиле DOS.

echo "Конец"

exit 0


Сценарий, начинающийся с #!/bin/sh, не может работать в режиме полной совместимости с Bash. Некоторые из специфических функций, присущих Bash, могут оказаться запрещенными к использованию. Сценарий, который требует полного доступа ко всем расширениям, имеющимся в Bash, должен начинаться строкой #!/bin/bash.

Сценарий не может экспортировать переменные родительскому процессу - оболочке. Здесь как в природе, потомок может унаследовать черты родителя, но не наооборот.

WHATEVER=/home/bozo
export WHATEVER
exit 0
bash$ echo $WHATEVER

bash$
Будьте уверены -- при выходе в командную строку переменная $WHATEVER останется неинициализированной.

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

Пример 31-1. Западня в подоболочке

#!/bin/bash
# Западня в подоболочке.

outer_variable=внешняя_переменная
echo
echo "outer_variable = $outer_variable"
echo

(
# Запуск в подоболочке

echo "внутри подоболочки outer_variable = $outer_variable"
inner_variable=внутренняя_переменная  # Инициализировать
echo "внутри подоболочки inner_variable = $inner_variable"
outer_variable=внутренняя_переменная  # Как думаете? Изменит внешнюю переменную?
echo "внутри подоболочки outer_variable = $outer_variable"

# Выход из подоболочки
)

echo
echo "за пределами подоболочки inner_variable = $inner_variable"  # Ничего не выводится.
echo "за пределами подоболочки outer_variable = $outer_variable"  # внешняя_переменная.
echo

exit 0

Передача вывода от echo по конвейеру команде read может давать неожиданные результаты. В этом сценарии, команда read действует так, как будто бы она была запущена в подоболочке. Вместо нее лучше использовать команду set (см. Пример 11-14).

Пример 31-2. Передача вывода от команды echo команде read, по конвейеру

#!/bin/bash
#  badread.sh:
#  Попытка использования 'echo' и 'read'
#+ для записи значений в переменные.

a=aaa
b=bbb
c=ccc

echo "один два три" | read a b c
# Попытка записать значения в переменные a, b и c.

echo
echo "a = $a"  # a = aaa
echo "b = $b"  # b = bbb
echo "c = $c"  # c = ccc
# Присваивания не произошло.

# ------------------------------

# Альтернативный вариант.

var=`echo "один два три"`
set -- $var
a=$1; b=$2; c=$3

echo "-------"
echo "a = $a"  # a = один
echo "b = $b"  # b = два
echo "c = $c"  # c = три
# На этот раз все в порядке.

# ------------------------------

#  Обратите внимание: в подоболочке 'read', для первого варианта, переменные присваиваются нормально.
#  Но только в подоболочке.

a=aaa          # Все сначала.
b=bbb
c=ccc

echo; echo
echo "один два три" | ( read a b c;
echo "Внутри подоболочки: "; echo "a = $a"; echo "b = $b"; echo "c = $c" )
# a = один
# b = два
# c = три
echo "-------"
echo "Снаружи: "
echo "a = $a"  # a = aaa
echo "b = $b"  # b = bbb
echo "c = $c"  # c = ccc
echo

exit 0

Огромный риск, для безопасности системы, представляет использование в скриптах команд, с установленным битом "suid". [1]

Использование сценариев в качестве CGI-приложений может приводить к серьезным проблемам из-за отсутствия контроля типов переменных. Более того, они легко могут быть заменены взломщиком на его собственные сценарии.

Bash не совсем корректно обрабатывает строки, содержащие двойной слэш (//).

Сценарии на языке Bash, созданные для Linux или BSD систем, могут потребовать доработки, перед тем как они смогут быть запущены в коммерческой версии UNIX. Такие сценарии, как правило, используют GNU-версии команд и утилит, которые имеют лучшую функциональность, нежели их аналоги в UNIX. Это особенно справедливо для таких утилит обработки текста, как tr.

 

Danger is near thee --

Beware, beware, beware, beware.

Many brave hearts are asleep in the deep.

So beware --

Beware.

  A.J. Lamb and H.W. Petrie

Примечания

[1]

Установка этого бита на файлы сценариев не имеет никакого эффекта.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 32. Стиль программирования

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

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

32.1. Неофициальные рекомендации по оформлению сценариев

 

... читая исходные тексты сценариев на Bourne shell (/bin/sh). Я был потрясен тем, насколько непонятно и загадочно могут выглядеть очень простые алгоритмы из-за неправильного оформления кода. Я не раз спрашивал себя: "Неужели кто-то может гордиться таким кодом?"

  Landon Noll

Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 33. Разное

 

Практически никто не знает грамматики Bourne shell-а. Даже изучение исходных текстов не дает ее полного понимания.

  Tom Duff

33.1. Интерактивный и неинтерактивный режим работы

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

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

#!/bin/bash
MY_PROMPT='$ '
while :
do
  echo -n "$MY_PROMPT"
  read line
  eval "$line"
  done

exit 0

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


Будем считать интерактивным такой сценарий, который может принимать ввод от пользователя, обычно с помощью команды read (см. Пример 11-2). В "реальной жизни" все намного сложнее. Пока же, будем придерживаться предположения о том, что интерактивный сценарий ограничен рамками tty, с которого сценарий был запущен пользователемa, т.е консоль или окно xterm.

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

Неинтерактивные сценарии прекрасно могут работать в фоне, в то время, как интерактивные -- подвисают, останавливаясь на операциях, ожидающих ввода пользователя. Сложности, возникающие с запуском интерактивных сценариев в фоновом режиме, могут быть преодолены с помощью expect-сценария или встроенного документа. В простейших случаях, можно организовать перенаправление ввода из файла в команду read (read variable <file). Эти приемы позволят создавать сценарии, которые смогут работать как в интерактивном, так и в неинтерактивном режимах.

Если внутри сценария необходимо проверить режим работы -- интерактивный или неинтерактивный, это можно сделать проверкой переменной окружения $PS1.

if [ -z $PS1 ] # интерактивный режим?
then
  # неинтерактивный
  ...
else
  # интерактивный
  ...
fi
Еще один способ -- проверка установки флага "i" в переменной $-.
case $- in
*i*)    # интерактивный режим
;;
*)      # неинтерактивный режим
;;
# (Из "UNIX F.A.Q.," 1993)


Note

Сценарий может принудительно запускаться в интерактивном режиме, для этого необходимо указать ключ -i в строке-заголовке #!/bin/bash -i. Однако вы должны помнить о том, что в таких случаях сценарий может выдавать сообщения об ошибках даже тогда, когда ошибок, по сути, нет.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 34. Bash, версия 2

Текущая версия Bash, та, которая скорее всего установлена в вашей системе, фактически -- 2.XX.Y.

bash$ echo $BASH_VERSION
2.05.8(1)-release
             
В этой версии классического языка сценариев Bash были добавлены переменные-массивы, [1] расширение строк и подстановка параметров, улучшен метод косвенных ссылок на переменные.

Пример 34-1. Расширение строк

#!/bin/bash

# "Расширение" строк (String expansion).
# Введено в Bash, начиная с версии 2.

# Строки вида  $'xxx'
# могут содержать дополнительные экранированные символы.

echo $'Звонок звенит 3 раза \a \a \a'
echo $'Три перевода формата \f \f \f'
echo $'10 новых строк \n\n\n\n\n\n\n\n\n\n'

exit 0

Пример 34-2. Косвенные ссылки на переменные -- новый метод

#!/bin/bash

# Косвенные ссылки на переменные.


a=letter_of_alphabet
letter_of_alphabet=z

echo "a = $a"           # Прямая ссылка.

echo "Now a = ${!a}"    # Косвенная ссылка.
# Форма записи ${!variable} намного удобнее старой "eval var1=\$$var2"

echo

t=table_cell_3
table_cell_3=24
echo "t = ${!t}"        # t = 24
table_cell_3=387
echo "Значение переменной t изменилось на ${!t}"    # 387

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

exit 0

Пример 34-3. Простая база данных, с применением косвенных ссылок

#!/bin/bash
# resistor-inventory.sh
# Простая база данных, с применением косвенных ссылок.

# ============================================================== #
# Данные

B1723_value=470                                   # сопротивление (Ом)
B1723_powerdissip=.25                             # рассеиваемая мощность (Вт)
B1723_colorcode="желтый-фиолетовый-коричневый"    # цветовая маркировка
B1723_loc=173                                     # где
B1723_inventory=78                                # количество (шт)

B1724_value=1000
B1724_powerdissip=.25
B1724_colorcode="коричневый-черный-красный"
B1724_loc=24N
B1724_inventory=243

B1725_value=10000
B1725_powerdissip=.25
B1725_colorcode="коричневый-черный-оранжевый"
B1725_loc=24N
B1725_inventory=89

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


echo

PS3='Введите ноиер: '

echo

select catalog_number in "B1723" "B1724" "B1725"
do
  Inv=${catalog_number}_inventory
  Val=${catalog_number}_value
  Pdissip=${catalog_number}_powerdissip
  Loc=${catalog_number}_loc
  Ccode=${catalog_number}_colorcode

  echo
  echo "Номер по каталогу $catalog_number:"
  echo "Имеется в наличии ${!Inv} шт. [${!Val} Ом / ${!Pdissip} Вт]."
  echo "Находятся в лотке # ${!Loc}."
  echo "Цветовая маркировка: \"${!Ccode}\"."

  break
done

echo; echo

# Упражнение:
# ----------
# Переделайте этот сценарий так, чтобы он использовал массивы вместо косвенных ссылок.
# Какой из вариантов более простой и интуитивный?


# Примечание:
# ----------
#  Язык командной оболочки не очень удобен для написания приложений,
#+ работающих с базами данных.
#  Для этой цели лучше использовать языки программирования, имеющие
#+ развитые средства для работы со структурами данных,
#+ такие как C++ или Java (может быть Perl).

exit 0

Пример 34-4. Массивы и другие хитрости для раздачи колоды карт в четыре руки

#!/bin/bash
# На старых системах может потребоваться вставить #!/bin/bash2.

# Карты:
# раздача в четыре руки.

UNPICKED=0
PICKED=1

DUPE_CARD=99

LOWER_LIMIT=0
UPPER_LIMIT=51
CARDS_IN_SUIT=13
CARDS=52

declare -a Deck
declare -a Suits
declare -a Cards
# Проще и понятнее было бы, имей мы дело
# с одним 3-мерным массивом.
# Будем надеяться, что в будущем, поддержка многомерных массивов будет введена в Bash.


initialize_Deck ()
{
i=$LOWER_LIMIT
until [ "$i" -gt $UPPER_LIMIT ]
do
  Deck[i]=$UNPICKED   # Пометить все карты в колоде "Deck", как "невыданная".
  let "i += 1"
done
echo
}

initialize_Suits ()
{
Suits[0]=Т # Трефы
Suits[1]=Б # Бубны
Suits[2]=Ч # Червы
Suits[3]=П # Пики
}

initialize_Cards ()
{
Cards=(2 3 4 5 6 7 8 9 10 В Д K Т)
# Альтернативный способ инициализации массива.
}

pick_a_card ()
{
card_number=$RANDOM
let "card_number %= $CARDS"
if [ "${Deck[card_number]}" -eq $UNPICKED ]
then
  Deck[card_number]=$PICKED
  return $card_number
else
  return $DUPE_CARD
fi
}

parse_card ()
{
number=$1
let "suit_number = number / CARDS_IN_SUIT"
suit=${Suits[suit_number]}
echo -n "$suit-"
let "card_no = number % CARDS_IN_SUIT"
Card=${Cards[card_no]}
printf %-4s $Card
# Вывод по столбцам.
}

seed_random ()  # Переустановка генератора случайных чисел.
{
seed=`eval date +%s`
let "seed %= 32766"
RANDOM=$seed
}

deal_cards ()
{
echo

cards_picked=0
while [ "$cards_picked" -le $UPPER_LIMIT ]
do
  pick_a_card
  t=$?

  if [ "$t" -ne $DUPE_CARD ]
  then
    parse_card $t

    u=$cards_picked+1
    # Возврат к индексации с 1 (временно).
    let "u %= $CARDS_IN_SUIT"
    if [ "$u" -eq 0 ]   # вложенный if/then.
    then
     echo
     echo
    fi
    # Смена руки.

    let "cards_picked += 1"
  fi
done

echo

return 0
}


# Структурное программирование:
# вся логика приложения построена на вызове функций.

#================
seed_random
initialize_Deck
initialize_Suits
initialize_Cards
deal_cards

exit 0
#================



# Упражнение 1:
# Добавьте комментарии, чтобы до конца задокументировать этот сценарий.

# Упражнение 2:
# Исправьте сценарий так, чтобы карты в каждой руке выводились отсортированными по масти.
# Вы можете добавить и другие улучшения.

# Упражнение 3:
# Упростите логику сценария.

Примечания

[1]

Chet Ramey обещал ввести в Bash ассоциативные массивы (они хорошо знакомы программистам, работающим с языком Perl) в одном из следующих релизов Bash.


Глава 35. Замечания и дополнения

35.1. От автора

Как я пришел к мысли о написании этой книги? Это необычная история. Случилось это лет несколько тому назад. Мне потребовалось изучить язык командной оболочки -- а что может быть лучше, как не чтение хорошей книги!? Я надеялся купить учебник и справочник, которые охватывали бы в полной мере данную тематику. Я искал книгу, которая возьмет трудные понятия, вывернет их наизнанку и подробно разжует на хорошо откомментированных примерах. В общем, я искал очень хорошую книгу. К сожалению, в природе таковой не существовало, поэтому я счел необходимым написать ее.

Это напоминает мне сказку о сумасшедшем профессоре. Помешанный, до безумия, при виде книги, любой книги -- в библиотеке, в книжном магазине -- не важно где, им овладевала уверенность в том, что и он мог бы написать эту книгу, причем сделать это гораздо лучше. Он стремительно мчался домой и садился за создание своей собственной книги с тем же названием. Когда он умер, в его доме нашли несколько тысяч, написанных им книг, этого количества хватило бы, чтобы посрамить самого Айзека Азимова. Книги, может быть и не были так хороши -- кто знает, но разве это имеет какое-то значение? Вот -- человек, жил своими грезами, пусть одержимый и движимый ими, но я не могу удержаться от восхищения старым чудаком...


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 4. Переменные и параметры. Введение.

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

4.1. Подстановка переменных

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

$

Необходимо всегда помнить о различиях между именем переменной и ее значением. Если variable1 -- это имя переменной, то $variable1 -- это ссылка на ее значение. "Чистые" имена переменных, без префикса $, могут использоваться только при объявлении переменный, при присваивании переменной некоторого значения, при удалении (сбросе), при экспорте и в особых случаях -- когда переменная представляет собой название сигнала (см. Пример 29-5). Присваивание может производится с помощью символа = (например: var1=27), инструкцией read и в заголовке цикла (for var2 in 1 2 3).

Заключение ссылки на переменную в двойные кавычки (" ") никак не сказывается на работе механизма подстановки. Этот случай называется "частичные кавычки", иногда можно встретить название "нестрогие кавычки". Одиночные кавычки (' ') заставляют интерпретатор воспринимать ссылку на переменную как простой набор символов, потому в одинарных кавычках операции подстановки не производятся. Этот случай называется "полные", или "строгие" кавычки. Дополнительную информацию вы найдете в Глава 5.

Примечательно, что написание $variable фактически является упрощенной формой написания ${variable}. Более строгая форма записи ${variable} может с успехом использоваться в тех случаях, когда применение упрощенной формы записи порождает сообщения о синтаксических ошибках (см. Section 9.3, ниже).

Пример 4-1. Присваивание значений переменным и подстановка значений переменных

#!/bin/bash

# Присваивание значений переменным и подстановка значений переменных

a=375
hello=$a

#-------------------------------------------------------------------------
# Использование пробельных символов
# с обеих сторон символа "=" присваивания недопустимо.

#  Если записать "VARIABLE =value",
#+ то интерпретатор попытается выполнить команду "VARIABLE" с параметром "=value".

#  Если записать "VARIABLE= value",
#+ то интерпретатор попытается установить переменную окружения "VARIABLE" в ""
#+ и выполнить команду "value".
#-------------------------------------------------------------------------


echo hello    # Это не ссылка на переменную, выведет строку "hello".

echo $hello
echo ${hello} # Идентично предыдущей строке.

echo "$hello"
echo "${hello}"

echo

hello="A B  C   D"
echo $hello   # A B C D
echo "$hello" # A B  C   D
# Здесь вы сможете наблюдать различия в выводе echo $hello и echo "$hello".
# Заключение ссылки на переменную в кавычки сохраняет пробельные символы.

echo

echo '$hello'  # $hello
# Внутри одинарных кавычек не производится подстановка значений переменных,
#+ т.е. "$" интерпретируется как простой символ.

# Обратите внимание на различия, существующие между этими типами кавычек.


hello=    # Запись пустого значения в переменную.
echo "\$hello (пустое значение) = $hello"
#  Обратите внимание: запись пустого значения -- это не то же самое,
#+ что сброс переменной, хотя конечный результат -- тот же (см. ниже).

# --------------------------------------------------------------

#  Допускается присваивание нескольких переменных в одной строке,
#+ если они отделены пробельными символами.
#  Внимание! Это может снизить читабельность сценария и оказаться непереносимым.

var1=variable1  var2=variable2  var3=variable3
echo
echo "var1=$var1   var2=$var2  var3=$var3"

# Могут возникнуть проблемы с устаревшими версиями "sh".

# --------------------------------------------------------------

echo; echo

numbers="один два три"
other_numbers="1 2 3"
# Если в значениях переменных встречаются пробелы,
# то использование кавычек обязательно.
echo "numbers = $numbers"
echo "other_numbers = $other_numbers"   # other_numbers = 1 2 3
echo

echo "uninitialized_variable = $uninitialized_variable"
# Неинициализированная переменная содержит "пустое" значение.
uninitialized_variable=   #  Объявление неинициализированной переменной
                          #+ (то же, что и присваивание пустого значения, см. выше).
echo "uninitialized_variable = $uninitialized_variable"
                          # Переменная содержит "пустое" значение.

uninitialized_variable=23       # Присваивание.
unset uninitialized_variable    # Сброс.
echo "uninitialized_variable = $uninitialized_variable"
                                # Переменная содержит "пустое" значение.

echo

exit 0
Caution

Неинициализированная переменная хранит "пустое" значение - не ноль!. Использование неинициализированных переменных может приводить к ошибкам разного рода в процессе исполнения.

Не смотря на это в арифметических операциях допускается использовать неинициализированные переменные.

echo "$uninitialized"                                # (пустая строка)
let "uninitialized += 5"                             # Прибавить 5.
echo "$uninitialized"                                # 5

#  Заключение:
#  Неинициализированные переменные не имеют значения, однако
#+ в арифметических операциях за значение таких переменных принимается число 0.
#  Это недокументированная (и возможно непереносимая) возможность.
См. так же Пример 11-19.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 2. Для начала о Sha-Bang

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

Пример 2-1. cleanup: Сценарий очистки лог-файлов в /var/log

# cleanup
# Для работы сценария требуются права root.

cd /var/log
cat /dev/null > messages
cat /dev/null > wtmp
echo "Лог-файлы очищены."

Здесь нет ничего необычного, это простая последовательность команд, которая может быть набрана в командной строке с консоли или в xterm. Преимущество размещения последовательности команд в скрипте состоит в том, что вам не придется всякий раз набирать эту последовательность вручную. Кроме того, скрипты легко могут быть модифицированы или обобщены для разных применений.

Пример 2-2. cleanup: Расширенная версия предыдущего сценария.

#!/bin/bash
# cleanup, version 2
# Для работы сценария требуются права root.

LOG_DIR=/var/log
ROOT_UID=0     # Только пользователь с $UID 0 имеет привилегии root.
LINES=50       # Количество сохраняемых строк по-умолчанию.
E_XCD=66       # Невозможно сменить каталог?
E_NOTROOT=67   # Признак отсутствия root-привилегий.


if [ "$UID" -ne "$ROOT_UID" ]
then
  echo "Для работы сценария требуются права root."
  exit $E_NOTROOT
fi

if [ -n "$1" ]
# Проверка наличия аргумента командной строки.
then
  lines=$1
else
  lines=$LINES # Значение по-умолчанию, если число не задано в командной строке
fi


#  Stephane Chazelas предложил следующее,
#+ для проверки корректности аргумента, переданного из командной строки,
#+ правда это достаточно сложно для данного руководства.
#
#    E_WRONGARGS=65  # Не числовой аргумент
#
#    case "$1" in
#    ""      ) lines=50;;
#    *[!0-9]*) echo "Usage: `basename $0` file-to-cleanup"; exit $E_WRONGARGS;;
#    *       ) lines=$1;;
#    esac
#
#* Конец проверки корректности аргумента


cd $LOG_DIR

if [ `pwd` != "$LOG_DIR" ]  # или   if [ "$PWD" != "$LOG_DIR" ]
                            # Не в /var/log?
then
  echo "Невозможно перейти в каталог $LOG_DIR."
  exit $E_XCD
fi  # Проверка каталога перед очисткой лог-файлов.

# более эффективный вариант:
#
# cd /var/log || {
#   echo "Невозможно перейти в требуемый каталог." >&2
#   exit $E_XCD;
# }




tail -$lines messages > mesg.temp # Сохранить последние строки в лог-файле.
mv mesg.temp messages


# cat /dev/null > messages
#* Необходимость этой команды отпала, поскольку очистка выполняется выше.

cat /dev/null > wtmp  #  команды ': > wtmp' и '> wtmp'  имеют тот же эффект.
echo "Лог-файлы очищены."

exit 0
#  Возвращаемое значение 0
#+ указывает на успешное завершение работы сценария.

Если вы не желаете полностью вычищать системные логи, то выше представлена улучшенная версия предыдущего сценария. Здесь сохраняются последние несколько строк (по-умолчанию -- 50).

Если файл сценария начинается с последовательности #!, которая в мире UNIX называется sha-bang, то это указывает системе какой интерпретатор следует использовать для исполнения сценария. Это двухбайтовая последовательность, или [1] -- специальный маркер, определяющий тип сценария, в данном случае -- сценарий командной оболочки (см. man magic). Более точно, sha-bang определяет интерпретатор, который вызывается для исполнения сценария, это может быть командная оболочка (shell), иной интерпретатор или утилита. [2]

#!/bin/sh
#!/bin/bash
#!/usr/bin/perl
#!/usr/bin/tcl
#!/bin/sed -f
#!/usr/awk -f


Каждая, из приведенных выше сигнатур, приводит к вызову различных интерпретаторов, будь то /bin/sh -- командный интерпретатор по-умолчанию (bash для Linux-систем), либо иной. [3] При переносе сценариев с сигнатурой #!/bin/sh на другие UNIX системы, где в качестве командного интерпретатора задан другой shell, вы можете лишиться некоторых особенностей, присущих bash. Поэтому такие сценарии должны быть POSIX совместимыми. [4].

Обратите внимание на то, что сигнатура должна указывать правильный путь к интерпретатору, в противном случае вы получите сообщение об ошибке -- как правило это "Command not found".

Сигнатура #! может быть опущена, если вы не используете специфичных команд. Во втором примере (см. выше) использование сигнатуры #! обязательно, поскольку сценарий использует специфичную конструкцию присваивания значения переменной lines=50. Еще раз замечу, что сигнатура #!/bin/sh вызывает командный интерпретатор по-умолчанию -- /bin/bash в Linux-системах.

Important

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

if [ $# -ne Number_of_expected_args ]
then
  echo "Usage: `basename $0` whatever"
  exit $WRONG_ARGS
fi


2.1. Запуск сценария

Запустить сценарий можно командой sh scriptname [5] или bash scriptname. (Не рекомендуется запуск сценария командой sh <scriptname>, поскольку это запрещает использование устройства стандартного ввода stdin в скрипте). Более удобный вариант -- сделать файл скрипта исполняемым, командой chmod.

Это:

chmod 555 scriptname (выдача прав на чтение/исполнение любому пользователю в системе) [6]

или

chmod +rx scriptname (выдача прав на чтение/исполнение любому пользователю в системе)

chmod u+rx scriptname (выдача прав на чтение/исполнение только "владельцу" скрипта)



После того, как вы сделаете файл сценария исполняемым, вы можете запустить его примерно такой командой ./scriptname. [7] Если, при этом, текст сценария начинается с корректной сигнатуры ("sha-bang"), то для его исполнения будет вызван соответствующий интерпретатор.

И наконец, завершив отладку сценария, вы можете поместить его в каталог /usr/local/bin (естественно, что для этого вы должны обладать правами root), чтобы сделать его доступным для себя и других пользователей системы. После этого сценарий можно вызвать, просто напечатав название файла в командной строке и нажав клавишу [ENTER].

Примечания

[1]

Некоторые разновидности UNIX (основанные на 4.2BSD) требуют, чтобы эта последовательность состояла из 4-х байт, за счет добавления пробела после !, #! /bin/sh.

[2]

В shell-скриптах последовательность #! должна стоять самой первой и задает интерпретатор (sh или bash). Интерпретатор, в свою очередь, воспринимает эту строку как комментарий, поскольку она начинается с символа #.

Если в сценарии имеются еще такие же строки, то они воспринимаются как обычный комментарий.

#!/bin/bash

echo "Первая часть сценария."
a=1

#!/bin/bash
# Это *НЕ* означает запуск нового сценария.

echo "Вторая часть сценария."
echo $a  # Значение переменной $a осталось равно 1.


[3]

Эта особенность позволяет использовать различные хитрости.

#!/bin/rm
# Самоуничтожающийся сценарий.

# Этот скрипт ничего не делает -- только уничтожает себя.

WHATEVER=65

echo "Эта строка никогда не будет напечатана."

exit $WHATEVER  # Не имеет смысла, поскольку работа сценария завершается не здесь.


Попробуйте запустить файл README с сигнатурой #!/bin/more (предварительно не забудьте сделать его исполняемым).

[4]

Portable Operating System Interface, попытка стандартизации UNIX-подобных операционных систем.

[5]

Внимание: вызов Bash-скрипта с помощью команды sh scriptname отключает специфичные для Bash расширения, что может привести к появлению ошибки и аварийному завершению работы сценария.

[6]

Сценарий должен иметь как право на исполнение, так и право на чтение, поскольку shell должен иметь возможность прочитать скрипт.

[7]

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


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 5. Кавычки

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

bash$ ls -l [Vv]*
-rw-rw-r--    1 bozo  bozo       324 Apr  2 15:05 VIEWDATA.BAT
 -rw-rw-r--    1 bozo  bozo       507 May  4 14:25 vartrace.sh
 -rw-rw-r--    1 bozo  bozo       539 Apr 14 17:11 viewdata.sh

bash$ ls -l '[Vv]*'
ls: [Vv]*: No such file or directory


Note

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

bash$ grep '[Пп]ервая' *.txt
file1.txt:Это первая строка в file1.txt.
 file2.txt:Это Первая строка в file2.txt.


Примечательно, что "не окавыченный" вариант команды grep [Пп]ервая *.txt будет правильно исполняться в Bash, но не в tcsh.

Вообще, желательно использовать двойные кавычки (" ") при обращении к переменным. Это предотвратит интерпретацию специальных символов, которые могут содержаться в именах переменных, за исключением $, ` (обратная кавычка) и \ (escape -- обратный слэш). [1] То, что символ $ попал в разряд исключений, позволяет выполнять обращение к переменным внутри строк, ограниченных двойными кавычками ("$variable"), т.е. выполнять подстановку значений переменных (см. Пример 4-1, выше).

Двойные кавычки могут быть использованы для предотвращения разбиения строки на слова. [2] Заключение строки в кавычки приводит к тому, что она передается как один аргумент, даже если она содержит пробельные символы - разделители.

variable1="a variable containing five words"
COMMAND This is $variable1    # Исполнение COMMAND с 7 входными аргументами:
# "This" "is" "a" "variable" "containing" "five" "words"

COMMAND "This is $variable1"  # Исполнение COMMAND с одним входным аргументом:
# "This is a variable containing five words"


variable2=""    # Пустая переменная.

COMMAND $variable2 $variable2 $variable2        # Исполнение COMMAND без аргументов.
COMMAND "$variable2" "$variable2" "$variable2"  # Исполнение COMMAND с 3 "пустыми" аргументами.
COMMAND "$variable2 $variable2 $variable2"      # Исполнение COMMAND с 1 аргументом (и 2 пробелами).

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


Tip

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

Пример 5-1. Вывод "причудливых" переменных

#!/bin/bash
# weirdvars.sh: Вывод "причудливых" переменных

var="'(]\\{}\$\""
echo $var        # '(]\{}$"
echo "$var"      # '(]\{}$"     Никаких различий.

echo

IFS='\'
echo $var        # '(] {}$"     \ символ-разделитель преобразован в пробел.
echo "$var"      # '(]\{}$"

# Примеры выше предоставлены S.C.

exit 0

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

Note

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

echo "Why can't I write 's between single quotes"

echo

# Обходной метод.
echo 'Why can'\''t I write '"'"'s between single quotes'
#    |-------|  |----------|   |-----------------------|
# Три строки, ограниченных одинарными кавычками,
# и экранированные одиночные кавычки между ними.

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


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

Caution

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

Специальное назначение некоторых экранированных символов

используемых совместно с echo и sed
\n

перевод строки (новая строка)

\r

перевод каретки

\t

табуляция

\v

вертикальная табуляция

\b

забой (backspace)

\a

"звонок" (сигнал)

\0xx

ASCII-символ с кодом 0xx в восьмеричном виде)

Пример 5-2. Экранированные символы

#!/bin/bash
# escaped.sh: экранированные символы

echo; echo

echo "\v\v\v\v"      # Вывод последовательности символов \v\v\v\v.
# Для вывода экранированных символов следует использовать ключ -e.
echo "============="
echo "ВЕРТИКАЛЬНАЯ ТАБУЛЯЦИЯ"
echo -e "\v\v\v\v"   # Вывод 4-х вертикальных табуляций.
echo "=============="

echo "КАВЫЧКИ"
echo -e "\042"       # Выводит символ " (кавычки с восьмеричным кодом ASCII 42).
echo "=============="

# Конструкция $'\X' делает использование ключа -e необязательным.
echo; echo "НОВАЯ СТРОКА И ЗВОНОК"
echo $'\n'           # Перевод строки.
echo $'\a'           # Звонок (сигнал).

echo "==============="
echo "КАВЫЧКИ"
# Bash версии 2 и выше допускает использование конструкции $'\nnn'.
# Обратите внимание: здесь под '\nnn' подразумевается восьмеричное значение.
echo $'\t \042 \t'   # Кавычки (") окруженные табуляцией.

# В конструкции $'\xhhh' допускается использовать и шестнадцатеричные значения.
echo $'\t \x22 \t'  # Кавычки (") окруженные табуляцией.
# Спасибо Greg Keraunen, за это примечание.
# Ранние версии Bash допускали употребление конструкции в виде '\x022'.
echo "==============="
echo


# Запись ASCII-символов в переменную.
# ----------------------------------------
quote=$'\042'        # запись символа " в переменную.
echo "$quote Эта часть строки ограничена кавычками, $quote а эта -- нет."

echo

# Конкатенация ASCII-символов в переменную.
triple_underline=$'\137\137\137'  # 137 -- это восьмеричный код символа '_'.
echo "$triple_underline ПОДЧЕРКИВАНИЕ $triple_underline"

echo

ABC=$'\101\102\103\010'           # 101, 102, 103 это  A, B и C соответственно.
echo $ABC

echo; echo

escape=$'\033'                    # 033 -- восьмеричный код экранирующего символа.
echo "\"escape\" выводится как $escape"
#                                   вывод отсутствует.

echo; echo

exit 0

Еще один пример использования конструкции $' ' вы найдете в Пример 34-1.

\"

кавычки

echo "Привет"                    # Привет
echo "Он сказал: \"Привет\"."    # Он сказал: "Привет".


\$

символ доллара (если за комбинацией символов \$ следует имя переменной, то она не будет разыменована)

echo "\$variable01"  # выведет $variable01


\\

обратный слэш

echo "\\"  # выведет \


Note

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

                      #  Простое экранирование и кавычки
echo \z               #  z
echo \\z              # \z
echo '\z'             # \z
echo '\\z'            # \\z
echo "\z"             # \z
echo "\\z"            # \z

                      #  Подстановка команды
echo `echo \z`        #  z
echo `echo \\z`       #  z
echo `echo \\\z`      # \z
echo `echo \\\\z`     # \z
echo `echo \\\\\\z`   # \z
echo `echo \\\\\\\z`  # \\z
echo `echo "\z"`      # \z
echo `echo "\\z"`     # \z

                      # Встроенный документ
cat <<EOF
\z
EOF                   # \z

cat <<EOF
\\z
EOF                   # \z

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


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

variable=\
echo "$variable"
# Не работает - дает сообщение об ошибке:
# test.sh: : command not found
# В "чистом" виде экранирующий (escape) символ не может быть записан в переменную.
#
#  Фактически, в данном примере, происходит экранирование символа перевода строки
#+ в результате получается такая команда:   variable=echo "$variable"
#+                                          ошибочное присваивание

variable=\
23skidoo
echo "$variable"    #  23skidoo
                    #  Здесь все в порядке, поскольку вторая строка
                    #+ является нормальным, с точки зрения присваивания, выражением.

variable=\
#        \^    За escape-символом следует пробел
echo "$variable"        # пробел

variable=\\
echo "$variable"        # \

variable=\\\
echo "$variable"
# Не работает - сообщение об ошибке:
# test.sh: \: command not found
#
#  Первый escape-символ экранирует второй, а третий оказывается неэкранированным,
#+ результат тот же, что и в первом примере.

variable=\\\\
echo "$variable"        # \\
                        # Второй и четвертый escape-символы экранированы.
                        # Это нормально.


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

file_list="/bin/cat /bin/gzip /bin/more /usr/bin/less /usr/bin/emacs-20.7"
# Список файлов как аргумент(ы) командной строки.

# Добавить два файла в список и вывести список.
ls -l /usr/X11R6/bin/xsetroot /sbin/dump $file_list

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

# Что произойдет, если экранировать пробелы в списке?
ls -l /usr/X11R6/bin/xsetroot\ /sbin/dump\ $file_list
# Ошибка: первые три файла будут "слиты" воедино
# и переданы команде 'ls -l' как один аргумент
# потому что два пробела, разделяющие аргументы (слова) -- экранированы.


Кроме того, escape-символ позволяет писать многострочные команды. Обычно, каждая команда занимает одну строку, но escape-символ позволяет экранировать символ перевода строки, в результате чего одна команда может занимать несколько строк.

(cd /source/directory && tar cf - . ) | \
(cd /dest/directory && tar xpvf -)
# Команда копирования дерева каталогов.
# Разбита на две строки для большей удобочитаемости.

# Альтернативный вариант:
tar cf - -C /source/directory . |
tar xpvf - -C /dest/directory
# См. примечание ниже.
# (Спасибо Stephane Chazelas.)
Note

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



echo "foo
bar"
#foo
#bar

echo

echo 'foo
bar'    # Никаких различий.
#foo
#bar

echo

echo foo\
bar     # Перевод строки экранирован.
#foobar

echo

echo "foo\
bar"     # Внутри "нестрогих" кавычек символ "\" интерпретируется как экранирующий.
#foobar

echo

echo 'foo\
bar'     # В "строгих" кавычках обратный слэш воспринимается как обычный символ.
#foo\
#bar

# Примеры предложены Stephane Chazelas.


Примечания

[1]

Символ "!", помещенный в двойные кавычки, порождает сообщение об ошибке, если команда вводится с командной строки. Вероятно это связано с тем, что этот символ интерпретируется как попытка обращения к истории команд. Однако внутри сценариев такой прием проблем не вызывает.

Не менее любопытно поведение символа "\", употребляемого внутри двойных кавычек.

bash$ echo hello\!
hello!



bash$ echo "hello\!"
hello\!



bash$ echo -e x\ty
xty


bash$ echo -e "x\ty"
x       y
             
(Спасибо Wayne Pollock за пояснения.)

[2]

"Разбиение на слова", в данном случае это означает разделение строки символов на некоторое число аргументов.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 6. Завершение и код завершения

 

...эта часть Bourne shell покрыта мраком, тем не менее все пользуются ею.

  Chet Ramey

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

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

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

Note

Когда работа сценария завершается командой exit без параметров, то код возврата сценария определяется кодом возврата последней исполненной командой.

Код возврата последней команды хранится в специальной переменной $?. После исполнения кода функции, переменная $? хранит код завершения последней команды, исполненной в функции. Таким способом в Bash передается "значение, возвращаемое" функцией. После завершения работы сценария, код возврата можно получить, обратившись из командной строки к переменной $?, т.е. это будет код возврата последней команды, исполненной в сценарии.

Пример 6-1. завершение / код завершения

#!/bin/bash

echo hello
echo $?    # код возврата = 0, поскольку команда выполнилась успешно.

lskdf      # Несуществующая команда.
echo $?    # Ненулевой код возврата, поскольку команду выполнить не удалось.

echo

exit 113   # Явное указание кода возврата 113.
           # Проверить можно, если набрать в командной строке "echo $?"
           # после выполнения этого примера.

#  В соответствии с соглашениями, 'exit 0' указывает на успешное завершение,
#+ в то время как ненулевое значение означает ошибку.

Переменная $? особенно полезна, когда необходимо проверить результат исполнения команды (см. Пример 12-27 и Пример 12-13).

Note

Символ !, может выступать как логическое "НЕ" для инверсии кода возврата.

Пример 6-2. Использование символа ! для логической инверсии кода возврата

true  # встроенная команда "true".
echo "код возврата команды \"true\" = $?"     # 0

! true
echo "код возврата команды \"! true\" = $?"   # 1
# Обратите внимание: символ "!" от команды необходимо отделять пробелом.
#    !true   вызовет сообщение об ошибке "command not found"

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


Caution

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


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 7. Проверка условий

практически любой язык программирования включает в себя условные операторы, предназначенные для проверки условий, чтобы выбрать тот или иной путь развития событий в зависимости от этих условий. В Bash, для проверки условий, имеется команда test, различного вида скобочные операторы и условный оператор if/then.

7.1. Конструкции проверки условий

Пример 7-1. Что есть "истина"?

#!/bin/bash

echo

echo "Проверяется \"0\""
if [ 0 ]      # ноль
then
  echo "0 -- это истина."
else
  echo "0 -- это ложь."
fi            # 0 -- это истина.

echo

echo "Проверяется \"1\""
if [ 1 ]      # единица
then
  echo "1 -- это истина."
else
  echo "1 -- это ложь."
fi            # 1 -- это ложь.

echo

echo "Testing \"-1\""
if [ -1 ]     # минус один
then
  echo "-1 -- это истина."
else
  echo "-1 -- это ложь."
fi            # -1 -- это истина.

echo

echo "Проверяется \"NULL\""
if [ ]        # NULL (пустое условие)
then
  echo "NULL -- это истина."
else
  echo "NULL -- это ложь."
fi            # NULL -- это ложь.

echo

echo "Проверяется \"xyz\""
if [ xyz ]    # строка
then
  echo "Случайная строка -- это истина."
else
  echo "Случайная строка -- это ложь."
fi            # Случайная строка -- это истина.

echo

echo "Проверяется \"\$xyz\""
if [ $xyz ]   # Проверка, если $xyz это null, но...
              # только для неинициализированных переменных.
then
  echo "Неинициализированная переменная -- это истина."
else
  echo "Неинициализированная переменная -- это ложь."
fi            # Неинициализированная переменная -- это ложь.

echo

echo "Проверяется \"-n \$xyz\""
if [ -n "$xyz" ]            # Более корректный вариант.
then
  echo "Неинициализированная переменная -- это истина."
else
  echo "Неинициализированная переменная -- это ложь."
fi            # Неинициализированная переменная -- это ложь.

echo


xyz=          # Инициализирована пустым значением.

echo "Проверяется \"-n \$xyz\""
if [ -n "$xyz" ]
then
  echo "Пустая переменная -- это истина."
else
  echo "Пустая переменная -- это ложь."
fi            # Пустая переменная -- это ложь.


echo


# Кргда "ложь" истинна?

echo "Проверяется \"false\""
if [ "false" ]              #  это обычная строка "false".
then
  echo "\"false\" -- это истина." #+ и она истинна.
else
  echo "\"false\" -- это ложь."
fi            # "false" -- это истина.

echo

echo "Проверяется \"\$false\""  # Опять неинициализированная переменная.
if [ "$false" ]
then
  echo "\"\$false\" -- это истина."
else
  echo "\"\$false\" -- это ложь."
fi            # "$false" -- это ложь.
              # Теперь мв получили ожидаемый результат.


echo

exit 0

Упражнение. Объясните результаты, полученные в Пример 7-1.

if [ condition-true ]
then
   command 1
   command 2
   ...
else
   # Необязательная ветка (можно опустить, если в ней нет необходимости).
   # Дополнительный блок кода,
   # исполняемый в случае, когда результат проверки -- "ложь".
   command 3
   command 4
   ...
fi


Note

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

if [ -x "$filename" ]; then


Else if и elif

elif

elif -- это краткая форма записи конструкции else if. Применяется для построения многоярусных инструкций if/then.

if [ condition1 ]
then
   command1
   command2
   command3
elif [ condition2 ]
# То же самое, что и else if
then
   command4
   command5
else
   default-command
fi


Конструкция if test condition-true является точным эквивалентом конструкции if [ condition-true ], где левая квадратная скобка [ выполняет те же действия, что и команда test. Закрывающая правая квадратная скобка ] не является абсолютно необходимой, однако, более новые версии Bash требуют ее наличие.

Note

Команда test -- это встроенная команда Bash, которая выполняет проверки файлов и производит сравнение строк. Таким образом, в Bash-скриптах, команда test не вызывает внешнюю (/usr/bin/test) утилиту, которая является частью пакета sh-utils. Аналогично, [ не производит вызов утилиты /usr/bin/[, которая является символической ссылкой на /usr/bin/test.

bash$ type test
test is a shell builtin
bash$ type '['
[ is a shell builtin
bash$ type '[['
[[ is a shell keyword
bash$ type ']]'
]] is a shell keyword
bash$ type ']'
bash: type: ]: not found
             


Пример 7-2. Эквиваленты команды test -- /usr/bin/test, [ ], и /usr/bin/[

#!/bin/bash

echo

if test -z "$1"
then
  echo "Аргументы командной строки отсутствуют."
else
  echo "Первый аргумент командной строки: $1."
fi

echo

if /usr/bin/test -z "$1"      # Дает тот же рузультат, что и встроенная команда "test".
then
  echo "Аргументы командной строки отсутствуют."
else
  echo "Первый аргумент командной строки: $1."
fi

echo

if [ -z "$1" ]                # Функционально идентично вышеприведенному блоку кода.
#   if [ -z "$1"                эта конструкция должна работать, но...
#+  Bash выдает сообщение об отсутствующей закрывающей скобке.
then
  echo "Аргументы командной строки отсутствуют."
else
  echo "Первый аргумент командной строки: $1."
fi

echo

if /usr/bin/[ -z "$1"         # Функционально идентично вышеприведенному блоку кода.
# if /usr/bin/[ -z "$1" ]     # Работает, но выдает сообщение об ошибке.
then
  echo "Аргументы командной строки отсутствуют."
else
  echo "Первый аргумент командной строки: $1."
fi

echo

exit 0

Конструкция [[ ]] более универсальна, по сравнению с [ ]. Этот расширенный вариант команды test перекочевал в Bash из ksh88.

Note

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

file=/etc/passwd

if [[ -e $file ]]
then
  echo "Файл паролей найден."
fi


Tip

Конструкция [[ ... ]] более предпочтительна, нежели [ ... ], поскольку поможет избежать некоторых логических ошибок. Например, операторы &&, ||, < и > внутри [[ ]] вполне допустимы, в то время как внутри [ ] порождают сообщения об ошибках.

Note

Строго говоря, после оператора if, ни команда test, ни квадратные скобки ( [ ] или [[ ]] ) не являются обязательными.

dir=/home/bozo

if cd "$dir" 2>/dev/null; then   # "2>/dev/null" подавление вывода сообщений об ошибках.
  echo "Переход в каталог $dir выполнен."
else
  echo "Невозможно перейти в каталог $dir."
fi
Инструкция "if COMMAND" возвращает код возврата команды COMMAND.

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

var1=20
var2=22
[ "$var1" -ne "$var2" ] && echo "$var1 не равно $var2"

home=/home/bozo
[ -d "$home" ] || echo "каталог $home не найден."


Внутри (( )) производится вычисление арифметического выражения. Если результатом вычислений является ноль, то возвращается 1, или "ложь". Ненулевой результат дает код возврата 0, или "истина". То есть полная противоположность инструкциям test и [ ], обсуждавшимся выше.

Пример 7-3. Арифметические выражения внутри (( ))

#!/bin/bash
# Проверка арифметических выражений.

# Инструкция (( ... )) вычисляет арифметические выражения.
# Код возврата противоположен коду возврата инструкции [ ... ] !

(( 0 ))
echo "Код возврата \"(( 0 ))\":  $?."         # 1

(( 1 ))
echo "Код возврата \"(( 1 ))\":  $?."         # 0

(( 5 > 4 ))                                   # true
echo "Код возврата \"(( 5 > 4 ))\":  $?."     # 0

(( 5 > 9 ))                                   # false
echo "Код возврата \"(( 5 > 9 ))\":  $?."     # 1

(( 5 - 5 ))                                   # 0
echo "Код возврата \"(( 5 - 5 ))\":  $?."     # 1

(( 5 / 4 ))                                   # Деление, все в порядке
echo "Код возврата \"(( 5 / 4 ))\":  $?."     # 0

(( 1 / 2 ))                                   # Результат деления < 1.
echo "Код возврата \"(( 1 / 2 ))\":  $?."     # Округляется до 0.
                                              # 1

(( 1 / 0 )) 2>/dev/null                       # Деление на 0.
echo "Код возврата \"(( 1 / 0 ))\":  $?."     # 1

# Для чего нужна инструкция "2>/dev/null" ?
# Что произойдет, если ее убрать?
# Попробуйте убрать ее и выполнить сценарий.

exit 0

Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 8. Операции и смежные темы

8.1. Операторы

присваивание

variable assignment

Инициализация переменной или изменение ее значения

=

Универсальный оператор присваивания, пригоден как для сравнения целых чисел, так и для сравнения строк.

var=27
category=minerals  # Пробелы до и после оператора "=" -- недопустимы.


Caution

Пусть вас не смущает, что оператор присваивания ("="), по своему внешнему виду, совпадает с оператором сравнения (=).

#    Здесь знак "="  выступает в качестве оператора сравнения

if [ "$string1" = "$string2" ]
# if [ "X$string1" = "X$string2" ] более отказоустойчивый вариант,
# предохраняет от "сваливания" по ошибке в случае, когда одна из переменных пуста.
# (добавленные символы "X" компенсируют друг друга.)
then
   command
fi


арифметические операторы

+

сложение

-

вычитание

*

умножение

/

деление

**

возведение в степень

# В Bash, начиная с версии 2.02, был введен оператор возведения в степень -- "**".

let "z=5**3"
echo "z = $z"   # z = 125


%

модуль (деление по модулю), возвращает остаток от деления

bash$ echo `expr 5 % 3`
2
             


Этот оператор может применяться в алгоритмах генерации псевдослучайных чисел в заданном диапазоне (см. Пример 9-23 и Пример 9-25), для форматирования вывода на экран (см. Пример 25-10 и Пример A-7), и даже для генерации простых чисел (см. Пример A-18). На удивление часто операцию деления по модулю можно встретить в различных численных алгоритмах.

Пример 8-1. Наибольший общий делитель

#!/bin/bash
# gcd.sh: поиск наибольшего общего делителя
#         по алгоритму Эвклида

#  Под "наибольшим общим делителем" (нод) двух целых чисел
#+ понимается наибольшее целое число, которое делит оба делимых без остатка.

#  Алгоритм Эвклида выполняет последовательное деление.
#  В каждом цикле,
#+ делимое  <---  делитель
#+ делитель <---  остаток
#+ до тех пор, пока остаток не станет равным нулю (остаток = 0).
#+ The gcd = dividend, on the final pass.
#
#  Замечательное описание алгоритма Эвклида можно найти
#  на сайте Jim Loy, http://www.jimloy.com/number/euclids.htm.


# ------------------------------------------------------
# Проверка входных параметров
ARGS=2
E_BADARGS=65

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


gcd ()
{

                                 #  Начальное присваивание.
  dividend=$1                    #  В сущности, не имеет значения
  divisor=$2                     #+ какой из них больше.
                                 #  Почему?

  remainder=1                    #  Если переменные неинициализировать,
                                 #+ то работа сценария будет прервана по ошибке
                                 #+ в первом же цикле.

  until [ "$remainder" -eq 0 ]
  do
    let "remainder = $dividend % $divisor"
    dividend=$divisor            # Повторить цикл с новыми исходными данными
    divisor=$remainder
  done                           # алгоритм Эвклида

}                                # последнее $dividend и есть нод.


gcd $1 $2

echo; echo "НОД чисел $1 и $2 = $dividend"; echo


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

exit 0
+=

"плюс-равно" (увеличивает значение переменной на заданное число)

let "var += 5" значение переменной var будет увеличено на 5.

-=

"минус-равно" (уменьшение значения переменной на заданное число)

*=

"умножить-равно" (умножить значение переменной на заданное число, результат записать в переменную)

let "var *= 4" значение переменной var будет увеличено в 4 раза.

/=

"слэш-равно" (уменьшение значения переменной в заданное число раз)

%=

"процент-равно" (найти остаток от деления значения переменной на заданное число, результат записать в переменную)

Арифметические операторы очень часто используются совместно с командами expr и let.

Пример 8-2. Арифметические операции

#!/bin/bash
# От 1 до 6 пятью различными способами.

n=1; echo -n "$n "

let "n = $n + 1"   # let "n = n + 1"   тоже допустимо
echo -n "$n "

: $((n = $n + 1))
# оператор ":" обязателен, поскольку в противном случае, Bash будет
#+ интерпретировать выражение "$((n = $n + 1))" как команду.
echo -n "$n "

n=$(($n + 1))
echo -n "$n "

: $[ n = $n + 1 ]
# оператор ":" обязателен, поскольку в противном случае, Bash будет
#+ интерпретировать выражение "$[ n = $n + 1 ]" как команду.
# Не вызывает ошибки даже если "n" содержит строку.
echo -n "$n "

n=$[ $n + 1 ]
#  Не вызывает ошибки даже если "n" содержит строку.
#* Старайтесь избегать употребления такой конструкции,
#+ поскольку она уже давно устарела и не переносима.
echo -n "$n "; echo

# Спасибо Stephane Chazelas.

exit 0
Note

Целые числа в Bash фактически являются знаковыми длинными целыми (32-бит), с диапазоном изменений от -2147483648 до 2147483647. Если в результате какой либо операции эти пределы будут превышены, то результат получится ошибочным.

a=2147483646
echo "a = $a"      # a = 2147483646
let "a+=1"         # Увеличить "a" на 1.
echo "a = $a"      # a = 2147483647
let "a+=1"         # увеличить "a" еще раз, с выходом за границы диапазона.
echo "a = $a"      # a = -2147483648
                   #      ОШИБКА! (выход за границы диапазона)


Caution

Bash ничего не знает о существовании чисел с плавающей запятой. Такие числа, из-за наличия символа десятичной точки, он воспринимает как строки.

a=1.5

let "b = $a + 1.3"  # Ошибка.
# t2.sh: let: b = 1.5 + 1.3: syntax error in expression (error token is ".5 + 1.3")

echo "b = $b"       # b=1
Для работы с числами с плавающей запятой в сценариях можно использовать утилиту-калькулятор bc.

битовые операции. Битовые операции очень редко используются в сценариях командного интерпретатора. Их главное назначение, на мой взгляд, установка и проверка некоторых значений, читаемых из портов ввода-вывода и сокетов. "Битовые операции" гораздо более уместны в компилирующих языках программирования, таких как C и C++.

битовые операции

<<

сдвигает на 1 бит влево (умножение на 2)

<<=

"сдвиг-влево-равно"

let "var <<= 2" значение переменной var сдвигается влево на 2 бита (умножается на 4)

>>

сдвиг вправо на 1 бит (деление на 2)

>>=

"сдвиг-вправо-равно" (имеет смысл обратный <<=)

&

по-битовое И (AND)

&=

"по-битовое И-равно"

|

по-битовое ИЛИ (OR)

|=

"по-битовое ИЛИ-равно"

~

по-битовая инверсия

!

По-битовое отрицание

^

по-битовое ИСКЛЮЧАЮЩЕЕ ИЛИ (XOR)

^=

"по-битовое ИСКЛЮЧАЮЩЕЕ-ИЛИ-равно"

логические операции

&&

логическое И (and)

if [ $condition1 ] && [ $condition2 ]
# То же самое, что:  if [ $condition1 -a $condition2 ]
# Возвращает true если оба операнда condition1 и condition2 истинны...

if [[ $condition1 && $condition2 ]]    # То же верно
# Обратите внимание: оператор && не должен использоваться внутри [ ... ].


Note

оператор &&, в зависимости от контекста, может так же использоваться в И-списках для построения составных команд.

||

логическое ИЛИ (or)

if [ $condition1 ] || [ $condition2 ]
# То же самое, что:  if [ $condition1 -o $condition2 ]
# Возвращает true если хотя бы один из операндов истинен...

if [[ $condition1 || $condition2 ]]    # Also works.
# Обратите внимание: оператор || не должен использоваться внутри [ ... ].


Note

Bash производит проверку кода возврата КАЖДОГО из операндов в логических выражениях.

Пример 8-3. Построение сложных условий, использующих && и ||

#!/bin/bash

a=24
b=47

if [ "$a" -eq 24 ] && [ "$b" -eq 47 ]
then
  echo "Первая проверка прошла успешно."
else
  echo "Первая проверка не прошла."
fi

# ОКА:  if [ "$a" -eq 24 && "$b" -eq 47 ]
#          пытается выполнить  ' [ "$a" -eq 24 '
#          и терпит неудачу наткнувшись на ']'.
#
#    if [[ $a -eq 24 && $b -eq 24 ]]   это правильный вариант
#    (в строке 17 оператор "&&" имеет иной смысл, нежели в строке 6.)
#    Спасибо Stephane Chazelas.


if [ "$a" -eq 98 ] || [ "$b" -eq 47 ]
then
  echo "Вторая проверка прошла успешно."
else
  echo "Вторая проверка не прошла."
fi


#  Опции -a и -o предоставляют
#+ альтернативный механизм проверки условий.
#  Спасибо Patrick Callahan.


if [ "$a" -eq 24 -a "$b" -eq 47 ]
then
  echo "Третья проверка прошла успешно."
else
  echo "Третья проверка не прошла."
fi


if [ "$a" -eq 98 -o "$b" -eq 47 ]
then
  echo "Четвертая проверка прошла успешно."
else
  echo "Четвертая проверка не прошла."
fi


a=rhino
b=crocodile
if [ "$a" = rhino ] && [ "$b" = crocodile ]
then
  echo "Пятая проверка прошла успешно."
else
  echo "Пятая проверка не прошла."
fi

exit 0

Операторы && и || могут использоваться и в арифметических вычислениях.

bash$ echo $(( 1 && 2 )) $((3 && 0)) $((4 || 0)) $((0 || 0))
1 0 1 0
             


прочие операции

,

запятая

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

let "t1 = ((5 + 3, 7 - 1, 15 - 4))"
echo "t1 = $t1"               # t1 = 11

let "t2 = ((a = 9, 15 / 3))"  #  Выполняется присваивание "a" = 9,
                              #+ а затем вычисляется "t2".
echo "t2 = $t2    a = $a"     # t2 = 5    a = 9


Оператор запятая чаще всего находит применение в циклах for. См. Пример 10-12.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 3. Служебные символы

Служебные символы, используемые в текстах сценариев.

#

Комментарии. Строки, начинающиеся с символа # (за исключением комбинации #!) -- являются комментариями.

# Эта строка -- комментарий.


Комментарии могут располагаться и в конце строки с исполняемым кодом.

echo "Далее следует комментарий." # Это комментарий.


Комментариям могут предшествовать пробелы (пробел, табуляция).

       # Перед комментарием стоит символ табуляции.


Caution

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

Note

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

echo "Символ # не означает начало комментария."
echo 'Символ # не означает начало комментария.'
echo Символ \# не означает начало комментария.
echo А здесь символ # означает начало комментария.

echo ${PATH#*:}       # Подстановка -- не комментарий.
echo $(( 2#101011 ))  # База системы счисления -- не комментарий.

# Спасибо, S.C.
Кавычки " ' и \ экранируют действие символа #.

В операциях поиска по шаблону символ # так же не воспринимается как начало комментария.

;

Разделитель команд. [Точка-с-запятой] Позволяет записывать две и более команд в одной строке.

echo hello; echo there


Следует отметить, что символ ";" иногда так же как и # необходимо экранировать.

;;

Ограничитель в операторе выбора case . [Двойная-точка-с-запятой]

case "$variable" in
abc)  echo "$variable = abc" ;;
xyz)  echo "$variable = xyz" ;;
esac


.

команда "точка". Эквивалент команды source (см. Пример 11-18). Это встроенная команда bash.

.

"точка" может являться частью имени файла . Если имя файла начинается с точки, то это "скрытый" файл, т.е. команда ls при обычных условиях его не отображает.

bash$ touch .hidden-file
bash$ ls -l
total 10
 -rw-r--r--    1 bozo      4034 Jul 18 22:04 data1.addressbook
 -rw-r--r--    1 bozo      4602 May 25 13:58 data1.addressbook.bak
 -rw-r--r--    1 bozo       877 Dec 17  2000 employment.addressbook


bash$ ls -al
total 14
 drwxrwxr-x    2 bozo  bozo      1024 Aug 29 20:54 ./
 drwx------   52 bozo  bozo      3072 Aug 29 20:51 ../
 -rw-r--r--    1 bozo  bozo      4034 Jul 18 22:04 data1.addressbook
 -rw-r--r--    1 bozo  bozo      4602 May 25 13:58 data1.addressbook.bak
 -rw-r--r--    1 bozo  bozo       877 Dec 17  2000 employment.addressbook
 -rw-rw-r--    1 bozo  bozo         0 Aug 29 20:54 .hidden-file
               


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

bash$ pwd
/home/bozo/projects

bash$ cd .
bash$ pwd
/home/bozo/projects

bash$ cd ..
bash$ pwd
/home/bozo/
               


Символ точка довольно часто используется для обозначения каталога назначения в операциях копирования/перемещения файлов.

bash$ cp /home/bozo/current_work/junk/* .
               


.

Символ "точка" в операциях поиска. При выполнении поиска по шаблону , в регулярных выражениях, символ "точка" обозначает одиночный символ.

"

Двойные кавычки . В строке "STRING", ограниченной двойными кавычками не выполняется интерпретация большинства служебных символов, которые могут находиться в строке. см. Глава 5.

'

Одинарные кавычки . [Одинарные кавычки] 'STRING' экранирует все служебные символы в строке STRING. Это более строгая форма экранирования. Смотрите так же Глава 5.

,

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

let "t2 = ((a = 9, 15 / 3))"  # Присваивает значение переменной "a" и вычисляет "t2".


\

escape. [обратный слэш] Комбинация \X "экранирует" символ X. Аналогичный эффект имеет комбинация с "одинарными кавычками", т.е. 'X'. Символ \ может использоваться для экранирования кавычек " и '.

Более детальному рассмотрению темы экранирования посвящена Глава 5.

/

Разделитель, используемый в указании пути к каталогам и файлам. [слэш] Отделяет элементы пути к каталогам и файлам (например /home/bozo/projects/Makefile).

В арифметических операциях -- это оператор деления.

`

Подстановка команд. [обратные кавычки] Обратные кавычки могут использоваться для записи в переменную команды `command`.

:

пустая команда. [двоеточие] Это эквивалент операции "NOP" (no op, нет операции). Может рассматриваться как синоним встроенной команды true. Команда ":" так же является встроенной командой Bash, которая всегда возвращает "true" (0).

:
echo $?   # 0


Бесконечный цикл:

while :
do
   operation-1
   operation-2
   ...
   operation-n
done

# То же самое:
#    while true
#    do
#      ...
#    done


Символ-заполнитель в условном операторе if/then:

if condition
then :   # Никаких действий не производится и управление передается дальше
else
   take-some-action
fi


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

: ${username=`whoami`}
# ${username=`whoami`}   без символа : выдает сообщение об ошибке,
#                        если "username" не является командой...


Как символ-заполнитель для оператора вложенного документа. См. Пример 17-9.

В операциях с подстановкой параметров (см. Пример 9-13).

: ${HOSTNAME?} ${USER?} ${MAIL?}
#Вывод сообщения об ошибке, если одна или более переменных не определены.


В операциях замены подстроки с подстановкой значений переменных.

В комбинации с оператором > (оператор перенаправления вывода), усекает длину файла до нуля. Если указан несуществующий файл -- то он создается.

: > data.xxx   # Файл "data.xxx" -- пуст

# Тот же эффект имеет команда cat /dev/null >data.xxx
# Однако в данном случае не производится создание нового процесса, поскольку ":" является встроенной командой.
См. так же Пример 12-11.

В комбинации с оператором >> -- оператор перенаправления с добавлением в конец файла и обновлением времени последнего доступа (: >> new_file). Если задано имя несуществующего файла, то он создается. Эквивалентно команде touch.

Note

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

Символ : может использоваться для создания комментариев, хотя и не рекомендуется. Если строка комментария начинается с символа #, то такая строка не проверяется интерпретатором на наличие ошибок. Однако в случае оператора : это не так.

: Это комментарий, который генерирует сообщение об ошибке, ( if [ $x -eq 3] ).


Символ ":" может использоваться как разделитель полей в /etc/passwd и переменной $PATH.

bash$ echo $PATH
/usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/sbin:/usr/sbin:/usr/games


!

инверсия (или логическое отрицание) используемое в условных операторах. Оператор ! инвертирует код завершения команды, к которой он применен. (см. Пример 6-2). Так же используется для логического отрицания в операциях сравнения, например, операция сравнения "равно" ( = ), при использовании оператора отрицания, преобразуется в операцию сравнения -- "не равно" ( != ). Символ ! является зарезервированным ключевым словом BASH.

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

Кроме того, из командной строки оператор ! запускает механизм историй Bash (см. Приложение F). Примечательно, что этот механизм недоступен из сценариев (т.е. исключительно из командной строки).

*

символ-шаблон. [звездочка] Символ * служит "шаблоном" для подстановки в имена файлов. Одиночный символ * означает любое имя файла в заданном каталоге.

bash$ echo *
abs-book.sgml add-drive.sh agram.sh alias.sh
             


В регулярных выражениях токен * представляет любое количество (в том числе и 0) символов.

*

арифметический оператор. В арифметических выражениях символ * обозначает операцию умножения.

Двойная звездочка (два символа звездочки, следующих подряд друг за другом -- **), обозначает операцию возведения в степень.

?

Оператор проверки условия. В некоторых выражениях символ ? служит для проверки выполнения условия.

В конструкциях с двойными скобками, символ ? подобен трехместному оператору языка C. См. Пример 9-28.

В выражениях с подстановкой параметра, символ ? проверяет -- установлена ли переменная.

?

сивол-шаблон. Символ ? обозначает одиночный символ при подстановке в имена файлов. В регулярных выражениях служит для обозначения одиночного символа.

$

Подстановка переменной.

var1=5
var2=23skidoo

echo $var1     # 5
echo $var2     # 23skidoo


Символ $, предшествующий имени переменной, указывает на то, что будет получено значение переменной.

$

end-of-line (конец строки). В регулярных выражениях, символ "$" обозначает конец строки.

${}
$*, $@
$?

код завершения. Переменная $? хранит код завершения последней выполненной команды, функции или сценария.

$$

id процесса. Переменная $$ хранит id процесса сценария.

()

группа команд.

(a=hello; echo $a)


Important

Команды, заключенные в круглые скобки исполняются в дочернем процессе -- subshell-е.

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

a=123
( a=321; )

echo "a = $a"   # a = 123
# переменная "a" в скобках подобна локальной переменной.


инициализация массивов.

Array=(element1 element2 element3)


{xxx,yyy,zzz,...}

Фигурные скобки.

grep Linux file*.{txt,htm*}
# Поиск всех вхождений слова "Linux"
# в файлах "fileA.txt", "file2.txt", "fileR.html", "file-87.htm", и пр.


Команда интерпретируется как список команд, разделенных точкой с запятой, с вариациями, представленными в фигурных скобках. [1] При интерпретации имен файлов (подстановка) используются параметры, заключенные в фигурные скобки.

Caution

Использование неэкранированных или неокавыченных пробелов внутри фигурных скобок недопустимо.

echo {file1,file2}\ :{\ A," B",' C'}

file1 : A file1 : B file1 : C file2 : A file2 : B file2 : C

{}

Блок кода. [фигурные скобки] Известен так же как "вложенный блок", эта конструкция, фактически, создает анонимную функцию. Однако, в отличии от обычных функций, переменные, создаваемые во вложенных блоках кода, доступны объемлющему сценарию.

bash$ { local a; a=123; }
bash: local: can only be used in a function
             


a=123
{ a=321; }
echo "a = $a"   # a = 321   (значение, присвоенное во вложенном блоке кода)

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


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

Пример 3-1. Вложенные блоки и перенаправление ввода-вывода

#!/bin/bash
# Чтение строк из файла /etc/fstab.

File=/etc/fstab

{
read line1
read line2
} < $File

echo "Первая строка в $File :"
echo "$line1"
echo
echo "Вторая строка в $File :"
echo "$line2"

exit 0

Пример 3-2. Сохранение результата исполнения вложенного блока в файл

#!/bin/bash
# rpm-check.sh

# Запрашивает описание rpm-архива, список файлов, и проверяется возможность установки.
# Результат сохраняется в файле.
#
# Этот сценарий иллюстрирует порядок работы со вложенными блоками кода.

SUCCESS=0
E_NOARGS=65

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

{
  echo
  echo "Описание архива:"
  rpm -qpi $1       # Запрос описания.
  echo
  echo "Список файлов:"
  rpm -qpl $1       # Запрос списка.
  echo
  rpm -i --test $1  # Проверка возможности установки.
  if [ "$?" -eq $SUCCESS ]
  then
    echo "$1 может быть установлен."
  else
    echo "$1 -- установка невозможна!"
  fi
  echo
} > "$1.test"       # Перенаправление вывода в файл.

echo "Результаты проверки rpm-архива находятся в файле $1.test"

# За дополнительной информацией по ключам команды rpm см. man rpm.

exit 0
Note

В отличие от групп команд в (круглых скобках), описаных выше, вложенные блоки кода, заключенные в {фигурные скобки} исполняются в пределах того же процесса, что и сам скрипт (т.е. не вызывают запуск дочернего процесса -- subshell). [2]

{} \;

pathname -- полное имя файла (т.е. путь к файлу и его имя). Чаще всего используется совместно с командой find.

Note

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

[ ]

test.

Проверка истинности выражения, заключенного в квадратные скобки [ ]. Примечательно, что [ является частью встроенной команды test (и ее синонимом), И не имеет никакого отношения к "внешней" утилите /usr/bin/test.

[[ ]]

test.

Проверка истинности выражения, заключенного между [[ ]] (зарезервированное слово интерпретатора).

См. описание конструкции [[ ... ]] ниже.

[ ]

элемент массива.

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

Array[1]=slot_1
echo ${Array[1]}


[ ]

диапазон символов.

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

(( ))

двойные круглые скобки.

Вычисляется целочисленное выражение, заключенное между двойными круглыми скобками (( )).

См. обсуждение, посвященное конструкции (( ... )) .

> &> >& >> <

Конструкция scriptname >filename перенаправляет вывод scriptname в файл filename. Если файл filename уже существовал, то его прежнее содержимое будет утеряно.

Конструкция command &>filename перенаправляет вывод команды command, как со stdout, так и с stderr, в файл filename.

Конструкция command >&2 перенаправляет вывод со stdout на stderr.

Конструкция scriptname >>filename добавляет вывод scriptname к файлу filename. Если задано имя несуществующего файла, то он создается.

(command)>

<(command)

В операциях сравнения, символы "<" и ">" обозначают операции сравнения строк .

А так же -- операции сравнения целых чисел. См. так же Пример 12-6.

<<

перенаправление ввода на встроенный документ.

<, >

Посимвольное ASCII-сравнение.

veg1=carrots
veg2=tomatoes

if [[ "$veg1" < "$veg2" ]]
then
  echo "Не смотря на то, что в словаре слово $veg1 предшествует слову $veg2,"
  echo "это никак не отражает мои кулинарные предпочтения."
else
  echo "Интересно. Каким словарем вы пользуетесь?"
fi


\<, \>

bash$ grep '\<the\>' textfile

|

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

echo ls -l | sh
#  Передает вывод "echo ls -l" команлному интерпретатору shell,
#+ тот же результат дает простая команда "ls -l".


cat *.lst | sort | uniq
# Объединяет все файлы ".lst", сортирует содержимое и удаляет повторяющиеся строки.


В конвейер могут объединяться и сценарии на языке командной оболочки.

#!/bin/bash
# uppercase.sh : Преобразование вводимых символов в верхний регистр.

tr 'a-z' 'A-Z'
#  Диапазоны символов должны быть заключены в кавычки
#+ чтобы предотвратить порождение имен файлов от однобуквенных имен файлов.

exit 0
А теперь попробуем объединить в конвейер команду ls -l с этим сценарием.
bash$ ls -l | ./uppercase.sh
-RW-RW-R--    1 BOZO  BOZO       109 APR  7 19:49 1.TXT
 -RW-RW-R--    1 BOZO  BOZO       109 APR 14 16:48 2.TXT
 -RW-R--R--    1 BOZO  BOZO       725 APR 20 20:56 DATA-FILE
             


Note

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

cat file1 file2 | ls -l | sort
# Вывод команды "cat file1 file2" будет утерян.


Конвейер исполняется в дочернем процессе, а посему -- не имеет доступа к переменным сценария.

variable="initial_value"
echo "new_value" | read variable
echo "variable = $variable"     # variable = initial_value


Если одна из команд в конвейере завершается аварийно, то это приводит к аварийному завершению работы всего конвейера.

>|

принудительное перенаправление, даже если установлен ключ noclobber option.

||

логическая операция OR (логическое ИЛИ). В опрециях проверки условий, оператор || возвращает 0 (success), если один из операндов имеет значение true (ИСТИНА).

&

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

bash$ sleep 10 &
[1] 850
[1]+  Done                    sleep 10
             


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

Пример 3-3. Запуск цикла в фоновом режиме

#!/bin/bash
# background-loop.sh

for i in 1 2 3 4 5 6 7 8 9 10            # Первый цикл.
do
  echo -n "$i "
done & # Запуск цикла в фоне.
       # Иногда возможны случаи выполнения этого цикла после второго цикла.

echo   # Этот 'echo' иногда не отображается на экране.

for i in 11 12 13 14 15 16 17 18 19 20   # Второй цикл.
do
  echo -n "$i "
done

echo   # Этот 'echo' иногда не отображается на экране.

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

# Ожидается, что данный сценарий выведет следующую последовательность:
# 1 2 3 4 5 6 7 8 9 10
# 11 12 13 14 15 16 17 18 19 20

# Иногда возможен такой вариант:
# 11 12 13 14 15 16 17 18 19 20
# 1 2 3 4 5 6 7 8 9 10 bozo $
# (Второй 'echo' не был выполнен. Почему?)

# Изредка возможен такой вариант:
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# (Первый 'echo' не был выполнен. Почему?)

# Крайне редко встречается и такое:
# 11 12 13 1 2 3 4 5 6 7 8 9 10 14 15 16 17 18 19 20
# Второй цикл начал исполняться раньше первого.

exit 0
Caution

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

&&

Логическая операция AND (логическое И). В операциях проверки условий, оператор && возвращает 0 (success) тогда, и только тогда, когда оба операнда имеют значение true (ИСТИНА).

-

префикс ключа. С этого символа начинаются опциональные ключи команд.

COMMAND -[Option1][Option2][...]

ls -al

sort -dfu $filename

set -- $variable

if [ $file1 -ot $file2 ]
then
  echo "Файл $file1 был создан раньше чем $file2."
fi

if [ "$a" -eq "$b" ]
then
  echo "$a равно $b."
fi

if [ "$c" -eq 24 -a "$d" -eq 47 ]
then
  echo "$c равно 24, а $d равно 47."
fi


-

перенаправление из/в stdin или stdout. [дефис]

(cd /source/directory && tar cf - . ) | (cd /dest/directory && tar xpvf -)
# Перемещение полного дерева файлов и подкаталогов из одной директории в другую
# [спасибо Алану Коксу (Alan Cox) <a.cox@swansea.ac.uk>, за небольшие поправки]

# 1) cd /source/directory    Переход в исходный каталог, содержимое которого будет перемещено
# 2) &&                     "И-список": благодаря этому все последующие команды будут выполнены
#                            только тогда, когда 'cd' завершится успешно
# 3) tar cf - .              ключом 'c' архиватор 'tar' создает новый архив,
#                            ключом 'f' (file) и последующим '-' задается файл архива -- stdout,
#                            в архив помещается текущий каталог ('.') с вложенными подкаталогами.
# 4) |                       конвейер с ...
# 5) ( ... )                 subshell-ом (дочерним экземпляром командной оболочки)
# 6) cd /dest/directory      Переход в каталог назначения.
# 7) &&                     "И-список", см. выше
# 8) tar xpvf -              Разархивирование ('x'), с сохранением атрибутов "владельца" и прав доступа ('p') к файлам,
#                            с выдачей более подробных сообщений на stdout ('v'),
#                            файл архива -- stdin ('f' с последующим '-').
#
#                            Примечательно, что 'x' -- это команда, а 'p', 'v' и 'f' -- ключи
# Во как!



# Более элегантный вариант:
#   cd source-directory
#   tar cf - . | (cd ../target-directory; tar xzf -)
#
# cp -a /source/directory /dest     имеет тот же эффект.


bunzip2 linux-2.4.3.tar.bz2 | tar xvf -
# --разархивирование tar-файла--    | --затем файл передается утилите "tar"--
# Если у вас утилита "tar" не поддерживает работу с "bunzip2",
# тогда придется выполнять работу в два этапа, с использованием конвейера.
# Целью данного примера является разархивирование тарбола (tar.bz2) с исходными текстами ядра.


Обратите внимание, что в этом контексте "-" - не самостоятельный оператор Bash, а скорее опция, распознаваемая некоторыми утилитами UNIX (такими как tar, cat и т.п.), которые выводят результаты своей работы в stdout.

bash$ echo "whatever" | cat -
whatever


В случае, когда ожидается имя файла, тогда "-" перенаправляет вывод на stdout (вспомните пример с tar cf) или принимает ввод с stdin.

bash$ file
Usage: file [-bciknvzL] [-f namefile] [-m magicfiles] file...
             
Сама по себе команда file без параметров завершается с сообщением об ошибке.

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

bash$ file -
abc
standard input:              ASCII text

bash$ file -
#!/bin/bash
standard input:              Bourne-Again shell script text executable
             
Теперь команда принимает ввод пользователя со stdin и анализирует его.

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

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

grep Linux file1 | diff file2 -

И наконец пример использования служебного символа "-" с командой tar.

Пример 3-4. Резервное архивирование всех файлов, которые были изменены в течение последних суток

#!/bin/bash

# Резервное архивирование (backup) всех файлов в текущем каталоге,
# которые были изменены в течение последних 24 часов
#+ в тарболл (tarball) (.tar.gz - файл).

BACKUPFILE=backup
archive=${1:-$BACKUPFILE}
#  На случай, если имя архива в командной строке не задано,
#+ т.е. по-умолчанию имя архива -- "backup.tar.gz"

tar cvf - `find . -mtime -1 -type f -print` > $archive.tar
gzip $archive.tar
echo "Каталог $PWD заархивирован в файл \"$archive.tar.gz\"."


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

# Им предложен альтернативный код:
# -------------------------------------------------------------------
#   find . -mtime -1 -type f -print0 | xargs -0 tar rvf "$archive.tar"
#      используется версия GNU утилиты "find".


#   find . -mtime -1 -type f -exec tar rvf "$archive.tar" '{}' \;
#         более универсальный вариант, хотя и более медленный,
#         зато может использоваться в других версиях UNIX.
# -------------------------------------------------------------------


exit 0
Caution

Могут возникнуть конфликтные ситуации между опреатором перенаправления "-" и именами файлов, начинающимися с символа "-". Поэтому сценарий должен проверять имена файлов и предаварять их префиксом пути, например, ./-FILENAME, $PWD/-FILENAME или $PATHNAME/-FILENAME.

Если значение переменной начинается с символа "-", то это тоже может быть причиной появления ошибок.

var="-n"
echo $var
# В данном случае команда приобретет вид "echo -n" и ничего не выведет.


-

предыдущий рабочий каталог. [дефис] Команда cd - выполнит переход в предыдущий рабочий каталог, путь к которому хранится в переменной окружения $OLDPWD .

Caution

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

-

Минус. Знак минус в арифметических операциях.

=

Символ "равно". Оператор присваивания

a=28
echo $a   # 28


В зависимости от контекста применения, символ "=" может выступать в качестве оператора сравнения.

+

Плюс. Оператор сложения в арифметических операциях.

В зависимости от контекста применения, символ + может выступать как оператор регулярного выражения.

+

Ключ (опция). Дополнительный флаг для ключей (опций) команд.

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

%

модуль. Модуль (остаток от деления) -- арифметическая операция.

В зависимости от контекста применения, символ % может выступать в качестве шаблона.

~

домашний каталог. [тильда] Соответствует содержимому внутренней переменной $HOME. ~bozo -- домашний каталог пользователя bozo, а команда ls ~bozo выведет содержимое его домашнего каталога. ~/ -- это домашний каталог текущего пользователя, а команда ls ~/ выведет содержимое домашнего каталога текущего пользователя.

bash$ echo ~bozo
/home/bozo

bash$ echo ~
/home/bozo

bash$ echo ~/
/home/bozo/

bash$ echo ~:
/home/bozo:

bash$ echo ~nonexistent-user
~nonexistent-user
             


~+

текущий рабочий каталог. Соответствует содержимому внутренней переменной $PWD.

~-

предыдущий рабочий каталог. Соответствует содержимому внутренней переменной $OLDPWD.

^

начало-строки. В регулярных выражениях символ "^" задает начало строки текста.

Управляющий символ

изменяет поведение терминала или управляет выводом текста. Управляющий символ набирается с клавиатуры как комбинация CONTROL + <клавиша>.

  • Ctl-C

    Завершение выполнения процесса.

  • Ctl-D

    Выход из командного интерпретатора (log out) (аналог команды exit).

    "EOF" (признак конца файла). Этот символ может выступать в качестве завершающего при вводе с stdin.

  • Ctl-G

    "BEL" (звуковой сигнал -- "звонок").

  • Ctl-H

    Backspace -- удаление предыдущего символа.

    #!/bin/bash
    # Вставка символа Ctl-H в строку.
    
    a="^H^H"                  # Два символа Ctl-H (backspace).
    echo "abcdef"             # abcdef
    echo -n "abcdef$a "       # abcd f
    # Пробел в конце ^              ^ двойной шаг назад.
    echo -n "abcdef$a"        # abcdef
    # Пробела в конце нет             backspace не работает (почему?).
    # Результаты могут получиться совсем не те, что вы ожидаете.
    echo; echo
    


  • Ctl-J

    Возврат каретки.

  • Ctl-L

    Перевод формата (очистка экрана (окна) терминала). Аналогична команде clear.

  • Ctl-M

    Перевод строки.

  • Ctl-U

    Стирание строки ввода.

  • Ctl-Z

    Приостановка процесса.

Пробельный символ

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

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

$IFS -- переменная специального назначения. Содержит символы-разделители полей, используемые некоторыми командами. По-умолчанию -- пробельные символы.

Примечания

[1]

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

[2]

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

ls | { read firstline; read secondline; }
# Ошибка! Вложенный блок будет запущен в дочернем процессе,
# таким образом, вывод команды "ls" не может быть записан в переменные
# находящиеся внутри блока.
echo "Первая строка: $firstline; вторая строка: $secondline"  # Не работает!

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



Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 9. К вопросу о переменных

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

9.1. Внутренние переменные

Встроенные переменные
$BASH

путь к исполняемому файлу Bash

bash$ echo $BASH
/bin/bash


$BASH_VERSINFO[n]

это массив, состоящий из 6 элементов, и содержащий информацию о версии Bash. Очень похожа на переменную $BASH_VERSION, описываемую ниже.

# Информация о версии Bash:

for n in 0 1 2 3 4 5
do
  echo "BASH_VERSINFO[$n] = ${BASH_VERSINFO[$n]}"
done

# BASH_VERSINFO[0] = 2                      # Major version no.
# BASH_VERSINFO[1] = 05                     # Minor version no.
# BASH_VERSINFO[2] = 8                      # Patch level.
# BASH_VERSINFO[3] = 1                      # Build version.
# BASH_VERSINFO[4] = release                # Release status.
# BASH_VERSINFO[5] = i386-redhat-linux-gnu  # Architecture
                                            # (same as $MACHTYPE).


$BASH_VERSION

версия Bash, установленного в системе

bash$ echo $BASH_VERSION
2.04.12(1)-release
             


tcsh% echo $BASH_VERSION
BASH_VERSION: Undefined variable.
             


Проверка переменной $BASH_VERSION -- неплохой метод проверки типа командной оболочки, под которой исполняется скрипт. Переменная $SHELL не всегда дает правильный ответ.

$DIRSTACK

содержимое вершины стека каталогов (который управляется командами pushd и popd)

Эта переменная соответствует команде dirs, за исключением того, что dirs показывает полное содержимое всего стека каталогов.

$EDITOR

заданный по-умолчанию редактор, вызываемый скриптом, обычно vi или emacs.

$EUID

"эффективный" идентификационный номер пользователя (Effective User ID)

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

Caution

Значение переменной $EUID необязательно должно совпадать с содержимым переменной $UID.

$FUNCNAME

имя текущей функции

xyz23 ()
{
  echo "Исполняется функция $FUNCNAME."  # Исполняется функция xyz23.
}

xyz23

echo "FUNCNAME = $FUNCNAME"        # FUNCNAME =
                                   # Пустое (Null) значение за пределеми функций.


$GLOBIGNORE

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

$GROUPS

группы, к которым принадлежит текущий пользователь

Это список групп (массив) идентификационных номеров групп для текущего пользователя, как эо записано в /etc/passwd.

root# echo $GROUPS
0


root# echo ${GROUPS[1]}
1


root# echo ${GROUPS[5]}
6
             


$HOME

домашний каталог пользователя, как правило это /home/username (см. Пример 9-13)

$HOSTNAME

Сетевое имя хоста устанавливается командой hostname во время исполнения инициализирующих сценариев на загрузке системы. Внутренняя переменная $HOSTNAME Bash получает свое значение посредством вызова функции gethostname(). См. так же Пример 9-13.

$HOSTTYPE

тип машины

Подобно $MACHTYPE, идентифицирует аппаратную архитектуру.

bash$ echo $HOSTTYPE
i686
$IFS

разделитель полей во вводимой строке (IFS -- Input Field Separator)

По-умолчанию -- пробельный символ (пробел, табуляция и перевод строки), но может быть изменен, например, для разбора строк, в которых отдельные поля разделены запятыми. Обратите внимание: при составлении содержимого переменной $*, Bash использует первый символ из $IFS для разделения аргументов. См. Пример 5-1.

bash$ echo $IFS | cat -vte
$


bash$ bash -c 'set w x y z; IFS=":-;"; echo "$*"'
w:x:y:z
             


Caution

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

Пример 9-1. $IFS и пробельные символы

#!/bin/bash
# При использовании $IFS, пробельные символы обрабатываются иначе, чем все остальные.

output_args_one_per_line()
{
  for arg
  do echo "[$arg]"
  done
}

echo; echo "IFS=\" \""
echo "-------"

IFS=" "
var=" a  b c   "
output_args_one_per_line $var  # output_args_one_per_line `echo " a  b c   "`
#
# [a]
# [b]
# [c]


echo; echo "IFS=:"
echo "-----"

IFS=:
var=":a::b:c:::"               # То же самое, только пробелы зменены символом ":".
output_args_one_per_line $var
#
# []
# [a]
# []
# [b]
# [c]
# []
# []
# []

# То же самое происходит и с разделителем полей "FS" в awk.

# Спасибо Stephane Chazelas.

echo

exit 0


(Спасибо S. C., за разъяснения и примеры.)

$LC_COLLATE

Чаще всего устанавливается в .bashrc или /etc/profile, эта переменная задает порядок сортировки символов, в операциях подстановки имен файлов и в поиске по шаблону. При неверной настройке переменной LC_COLLATE можно получить весьма неожиданные результаты.

Note

Начиная с версии 2.05, Bash, в операциях подстановки имен файлов, не делает различий между символами верхнего и нижнего регистров, в диапазонах символов в квадратных скобках. Например,, ls [A-M]* выведет как File1.txt, так и file1.txt. Возврат к общепринятому стандарту поведения шаблонов в квадратных скобках выполняется установкой переменной LC_COLLATE в значение C командой export LC_COLLATE=C в файле /etc/profile и/или ~/.bashrc.

$LC_CTYPE

Эта внутренняя переменная определяет кодировку символов. Используется в операциях подстановки и поиске по шаблону.

$LINENO

Номер строки исполняемого сценария. Эта переменная имеет смысл только внутри исполняемого сценария и чаще всего применяется в отладочных целях.

# *** BEGIN DEBUG BLOCK ***
last_cmd_arg=$_  # Запомнить.

echo "Строка $LINENO: переменная \"v1\" = $v1"
echo "Последний аргумент командной строки = $last_cmd_arg"
# *** END DEBUG BLOCK ***


$MACHTYPE

аппаратная архитектура

Идентификатор аппаратной архитектуры.

bash$ echo $MACHTYPE
i686
$OLDPWD

прежний рабочий каталог ("OLD-Print-Working-Directory")

$OSTYPE

тип операционной системы

bash$ echo $OSTYPE
linux
$PATH

путь поиска, как правило включает в себя каталоги /usr/bin/, /usr/X11R6/bin/, /usr/local/bin, и т.д.

Когда командный интерпретатор получает команду, то он автоматически пытается отыскать соответствующий исполняемый файл в указанном списке каталогов (в переменной $PATH). Каталоги, в указанном списке, должны отделяться друг от друга двоеточиями. Обычно, переменная $PATH инициализируется в /etc/profile и/или в ~/.bashrc (см. Глава 26).

bash$ echo $PATH
/bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin:/sbin:/usr/sbin


Инструкция PATH=${PATH}:/opt/bin добавляет каталог /opt/bin в конец текущего пути поиска. Иногда может оказаться целесообразным, внутри сценария, временно добавить какой-либо каталог к пути поиска. По завершении работы скрипта, эти изменения будут утеряны (вспомните о том, что невозможно изменить переменные окружения вызывающего процесса).

Note

Текущий "рабочий каталог", ./, обычно не включается в $PATH из соображений безопасности.

$PIPESTATUS

Код возврата канала (конвейера). Интересно, что это не то же самое, что код возврата последней исполненной команды.

bash$ echo $PIPESTATUS
0

bash$ ls -al | bogus_command
bash: bogus_command: command not found
bash$ echo $PIPESTATUS
141

bash$ ls -al | bogus_command
bash: bogus_command: command not found
bash$ echo $?
127
             


Caution

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

tcsh% bash

bash$ who | grep nobody | sort
bash$ echo ${PIPESTATUS[*]}
0
             


Если поместить эти строки в сценарий и исполнить его, то будут выведены верные значения 0 1 0.

Спасибо Wayne Pollock за замечания и предоставленный пример.

$PPID

Переменная $PPID хранит PID (идентификатор) родительского процесса. [1]

Сравните с командой pidof.

$PS1

prompt, приглашение командной строки.

$PS2

Вторичное приглашение командной строки, выводится тогда, когда от пользователя ожидается дополнительный ввод. Отображается как ">".

$PS3

Третичное приглашение (prompt), выводится тогда, когда пользователь должен сделать выбор в операторе select (см. Пример 10-29).

$PS4

Приглашение (prompt) четвертого уровня, выводится в начале каждой строки вывода тогда, когда сценарий вызывается с ключом -x. Отображается как "+".

$PWD

рабочий (текущий) каталог

Аналог встроенной команды pwd.

#!/bin/bash

E_WRONG_DIRECTORY=73

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

TargetDirectory=/home/bozo/projects/GreatAmericanNovel

cd $TargetDirectory
echo "Удаление файлов в каталоге $TargetDirectory."

if [ "$PWD" != "$TargetDirectory" ]
then    # Защита от случайного удаления файлов не в том каталоге.
  echo "Неверный каталог!"
  echo "Переменная $PWD указывает на другой каталог!"
  exit $E_WRONG_DIRECTORY
fi

rm -rf *
rm .[A-Za-z0-9]*    # удалить "скрытые" файлы (начинающиеся с ".")
# rm -f .[^.]* ..?*   удалить файлы, чьи имена начинаются с нескольких точек.
# (shopt -s dotglob; rm -f *)   тоже работает верно.
# Спасибо S.C. за замечание.

# Имена файлов могут содержать любые символы из диапазона 0-255, за исключением "/".
# Оставляю вопрос удаления файлов с "необычными" символами для самостоятельного изучения.

# Здесь можно вставить дополнительные действия, по мере необходимости.

echo
echo "Конец."
echo "Файлы, из каталога $TargetDirectory, удалены."
echo


exit 0


$REPLY

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

#!/bin/bash

echo
echo -n "Ваше любимое растение? "
read

echo "Ваше любимое растение: $REPLY."
# REPLY хранит последнее значение, прочитанное командой "read" тогда, и только тогда
#+ когда команде "read" не передается имя переменной.

echo
echo -n "Ваш любимый фрукт? "
read fruit
echo "Ваш любимый фрукт $fruit."
echo "но..."
echo "Значение переменной \$REPLY осталось равным $REPLY."
# Переменная $REPLY не была перезаписана потому, что
# следующей команде "read", в качестве аргумента была передана переменная $fruit

echo

exit 0


$SECONDS

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

#!/bin/bash
# Автор: Mendel Cooper
# Дополнен переводчиком.
#

TIME_LIMIT=10
INTERVAL=1

echo
echo "Для прерывания работы сценария, ранее чем через $TIME_LIMIT секунд, нажмите Control-C."
echo

while [ "$SECONDS" -le "$TIME_LIMIT" ]
do
# Оригинальный вариант сценария содержал следующие строки
#  if [ "$SECONDS" -eq 1 ]
#  then
#    units=second
#  else
#    units=seconds
#  fi
#
# Однако, из-за того, что в русском языке для описания множественного числа
# существует большее число вариантов, чем в английском,
# переводчик позволил себе смелость несколько подправить сценарий
# (прошу ногами не бить! ;-) )
# === НАЧАЛО БЛОКА ИЗМЕНЕНИЙ, ВНЕСЕННЫХ ПЕРЕВОДЧИКОМ ===

  let "last_two_sym = $SECONDS - $SECONDS / 100 * 100" # десятки и единицы
  if [ "$last_two_sym" -ge 11 -a "$last_two_sym" -le 19 ]
  then
    units="секунд"               # для чисел, которые заканчиваются на "...надцать"
  else
    let "last_sym = $last_two_sym - $last_two_sym / 10 * 10"  # единицы
    case "$last_sym" in
      "1" )
        units="секунду"         # для чисел, заканчивающихся на 1
      ;;
      "2" | "3" | "4" )
        units="секунды"         # для чисел, заканчивающихся на 2, 3 и 4
      ;;
      * )
        units="секунд"          # для всех остальных (0, 5, 6, 7, 8, 9)
      ;;
    esac
  fi
# === КОНЕЦ БЛОКА ИЗМЕНЕНИЙ, ВНЕСЕННЫХ ПЕРЕВОДЧИКОМ ===

  echo "Сценарий отработал $SECONDS $units."
  #  В случае перегруженности системы, скрипт может перескакивать через отдельные
  #+  значения счетчика
  sleep $INTERVAL
done

echo -e "\a"  # Сигнал!

exit 0


$SHELLOPTS

список допустимых опций интерпретатора shell. Переменная доступна только для чтения.

bash$ echo $SHELLOPTS
braceexpand:hashall:histexpand:monitor:history:interactive-comments:emacs
             


$SHLVL

Уровень вложенности shell. Если в командной строке

echo $SHLVL
дает 1, то в сценарии значение этой переменной будет больше на 1, т.е. 2.

$TMOUT

Если переменная окружения $TMOUT содержит ненулевое значение, то интерпретатор будет ожидать ввод не более чем заданное число секунд, что, в первичном приглашении (см. описание PS1 выше), может привести к автоматическому завершению сеанса работы.

Note

К сожалению это возможно только во время ожидания ввода с консоли или в окне терминала. А как было бы здорово, если бы можно было использовать эту внутреннюю переменную, скажем в комбинации с командой read! Но в данном контексте эта переменная абсолютно не применима и потому фактически бесполезна в сценариях. (Есть сведения о том, что в ksh время ожидания ввода командой read можно ограничить.)

Организация ограничения времени ожидания ввода от пользователя в сценариях возможна, но это требут довольно сложных махинаций. Как один из вариантов, можно предложить организовать прерывание цикла ожидания по сигналу. Но это потребует написание функции обработки сигналов командой trap (см. Пример 29-5).

Пример 9-2. Ограничения времени ожидания ввода

#!/bin/bash
# timed-input.sh

# TMOUT=3            бесполезно в сценариях

TIMELIMIT=3  # Три секунды в данном случае, но может быть установлено и другое значение

PrintAnswer()
{
  if [ "$answer" = TIMEOUT ]
  then
    echo $answer
  else       # Чтобы не спутать разные варианты вывода.
    echo "Ваше любимое растение $answer"
    kill $!  # "Прибить" ненужную больше функцию TimerOn, запущенную в фоновом процессе.
             # $! -- PID последнего процесса, запущенного в фоне.
  fi

}



TimerOn()
{
  sleep $TIMELIMIT && kill -s 14 $$ &
  # Ждать 3 секунды, после чего выдать sigalarm сценарию.
}

Int14Vector()
{
  answer="TIMEOUT"
  PrintAnswer
  exit 14
}

trap Int14Vector 14   # переназначить процедуру обработки прерывания от таймера (14)

echo "Ваше любимое растение? "
TimerOn
read answer
PrintAnswer


#  По общему признанию, это не очень хороший способ ограничения времени ожидания,
#+ однако опция "-t"команды "read" упрощает задачу.
#  См. "t-out.sh", ниже.

#  Если вам нужно что-то более элегантное...
#+ подумайте о написании программы на C или C++,
#+ с использованием соответствующих библиотечных функций, таких как 'alarm' и 'setitimer'.

exit 0

В качестве альтернативы можно использовать stty.

Пример 9-3. Еще один пример ограничения времени ожидания ввода от пользователя

#!/bin/bash
# timeout.sh

# Автор: Stephane Chazelas,
# дополнен автором документа.

INTERVAL=5                # предел времени ожидания

timedout_read() {
  timeout=$1
  varname=$2
  old_tty_settings=`stty -g`
  stty -icanon min 0 time ${timeout}0
  eval read $varname      # или просто    read $varname
  stty "$old_tty_settings"
  # См. man stty.
}

echo; echo -n "Как Вас зовут? Отвечайте быстрее! "
timedout_read $INTERVAL your_name

# Такой прием может не работать на некоторых типах терминалов.
# Максимальное время ожидания зависит от терминала.
# (чаще всего это 25.5 секунд).

echo

if [ ! -z "$your_name" ]  # Если имя было введено...
then
  echo "Вас зовут $your_name."
else
  echo "Вы не успели ответить."
fi

echo

# Алгоритм работы этого сценария отличается от "timed-input.sh".
# Каждое нажатие на клавишу вызывает сброс счетчика в начальное состояние.

exit 0

Возможно самый простой способ -- использовать опцию -t команды read.

Пример 9-4. Ограничение времени ожидания команды read

#!/bin/bash
# t-out.sh 

TIMELIMIT=4        # 4 секунды

read -t $TIMELIMIT variable <&1

echo

if [ -z "$variable" ]
then
  echo "Время ожидания истекло."
else
  echo "variable = $variable"
fi  

exit 0
$UID

user id number

UID (идентификатор) текущего пользователя, в соответствии с /etc/passwd

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

Пример 9-5. Я -- root?

#!/bin/bash
# am-i-root.sh:   Root я, или не root?

ROOT_UID=0   # $UID root-а всегда равен 0.

if [ "$UID" -eq "$ROOT_UID" ]  # Настоящий "root"?
then
  echo "- root!"
else
  echo "простой пользователь (но мамочка вас тоже любит)!"
fi

exit 0


# ============================================================= #
#  Код, приведенный ниже, никогда не отработает,
#+ поскольку работа сценария уже завершилась выше

# Еще один способ отличить root-а от не root-а:

ROOTUSER_NAME=root

username=`id -nu`              # Или...   username=`whoami`
if [ "$username" = "$ROOTUSER_NAME" ]
then
  echo "Рутти-тутти. - root!"
else
  echo "Вы - лишь обычный юзер."
fi

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

Note

Переменные $ENV, $LOGNAME, $MAIL, $TERM, $USER и $USERNAME, не являются встроенными переменными Bash. Тем не менее, они часто инициализируются как переменные окружения в одном из стартовых файлов Bash. Переменная $SHELL, командная оболочка пользователя, может задаваться в /etc/passwd или в сценарии "init" и она тоже не является встроенной переменной Bash.

tcsh% echo $LOGNAME
bozo
tcsh% echo $SHELL
/bin/tcsh
tcsh% echo $TERM
rxvt

bash$ echo $LOGNAME
bozo
bash$ echo $SHELL
/bin/tcsh
bash$ echo $TERM
rxvt
             


Позиционные параметры (аргументы)

$0, $1, $2 и т.д.

аргументы передаются... из командной строки в сценарий, функциям или команде set (см. Пример 4-5 и Пример 11-13)

$#

количество аргументов командной строки [2], или позиционных параметров (см. Пример 33-2)

$*

Все аргументы в виде одной строки (слова)

$@

То же самое, что и $*, но при этом каждый параметр представлен как отдельная строка (слово), т.е. параметры не подвергаются какой либо интерпретации.

Пример 9-6. arglist: Вывод списка аргументов с помощью переменных $* и $@

#!/bin/bash
# Вызовите сценарий с несколькими аргументами, например: "один два три".

E_BADARGS=65

if [ ! -n "$1" ]
then
  echo "Порядок использования: `basename $0` argument1 argument2 и т.д."
  exit $E_BADARGS
fi

echo

index=1

echo "Список аргументов в переменной \"\$*\":"
for arg in "$*"  # Работает некорректно, если "$*" не ограничена кавычками.
do
  echo "Аргумент #$index = $arg"
  let "index+=1"
done             # $* воспринимает все аргументы как одну строку.
echo "Полный список аргументов выглядит как одна строка."

echo

index=1

echo "Список аргументов в переменной \"\$@\":"
for arg in "$@"
do
  echo "Аргумент #$index = $arg"
  let "index+=1"
done             # $@ воспринимает аргументы как отдельные строки (слова).
echo "Список аргументов выглядит как набор различных строк (слов)."

echo

exit 0

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

#!/bin/bash
# Вызовите сценарий в таком виде: ./scriptname 1 2 3 4 5

echo "$@"    # 1 2 3 4 5
shift
echo "$@"    # 2 3 4 5
shift
echo "$@"    # 3 4 5

# Каждая из команд "shift" приводит к потере аргумента $1,
# но остальные аргументы остаются в "$@".


Специальная переменная $@ может быть использована для выбора типа ввода в сценария. Команда cat "$@" позволяет выполнять ввод как со стандартного устройства ввода stdin, так и из файла, имя которого передается сценарию из командной строки. См. Пример 12-17 и Пример 12-18.

Caution

Переменные $* и $@, в отдельных случаях, могут содержать противоречивую информацию! Это зависит от содержимого переменной $IFS.

Пример 9-7. Противоречия в переменных $* и $@

#!/bin/bash

#  Демонстрация противоречивости содержимого внутренних переменных "$*" и "$@",
#+ которая проявляется при изменении порядка заключения параметров в кавычки.
#  Демонстрация противоречивости, проявляющейся при изменении
#+ содержимого переменной IFS.


set -- "Первый один" "второй" "третий:один" "" "Пятый: :один"
# Установка аргументов $1, $2, и т.д.

echo

echo 'IFS по-умолчанию, переменная "$*"'
c=0
for i in "$*"               # в кавычках
do echo "$((c+=1)): [$i]"   # Эта строка остается без изменений во всех циклах.
                            # Вывод аргументов.
done
echo ---

echo 'IFS по-умолчанию, переменная $*'
c=0
for i in $*                 # без кавычек
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS по-умолчанию, переменная "$@"'
c=0
for i in "$@"
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS по-умолчанию, переменная $@'
c=0
for i in $@
do echo "$((c+=1)): [$i]"
done
echo ---

IFS=:
echo 'IFS=":", переменная "$*"'
c=0
for i in "$*"
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", переменная $*'
c=0
for i in $*
do echo "$((c+=1)): [$i]"
done
echo ---

var=$*
echo 'IFS=":", переменная "$var" (var=$*)'
c=0
for i in "$var"
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", переменная $var (var=$*)'
c=0
for i in $var
do echo "$((c+=1)): [$i]"
done
echo ---

var="$*"
echo 'IFS=":", переменная $var (var="$*")'
c=0
for i in $var
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", переменная "$var" (var="$*")'
c=0
for i in "$var"
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", переменная "$@"'
c=0
for i in "$@"
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", переменная $@'
c=0
for i in $@
do echo "$((c+=1)): [$i]"
done
echo ---

var=$@
echo 'IFS=":", переменная $var (var=$@)'
c=0
for i in $var
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", переменная "$var" (var=$@)'
c=0
for i in "$var"
do echo "$((c+=1)): [$i]"
done
echo ---

var="$@"
echo 'IFS=":", переменная "$var" (var="$@")'
c=0
for i in "$var"
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", переменная $var (var="$@")'
c=0
for i in $var
do echo "$((c+=1)): [$i]"
done

echo

# Попробуйте запустить этот сценарий под ksh или zsh -y.

exit 0

# Это сценарий написан Stephane Chazelas,
# Незначительные изменения внесены автором документа.
Note

Различия между $@ и $* наблюдаются только тогда, когда они помещаются в двойные кавычки.

Пример 9-8. Содержимое $* и $@, когда переменная $IFS -- пуста

#!/bin/bash

# Если переменная $IFS инициализирована "пустым" значением,
# то "$*" и "$@" содержат аргументы не в том виде, в каком ожидается.

mecho ()       # Вывод аргументов.
{
echo "$1,$2,$3";
}


IFS=""         # Инициализация "пустым" значением.
set a b c      # Установка аргументов.

mecho "$*"     # abc,,
mecho $*       # a,b,c

mecho $@       # a,b,c
mecho "$@"     # a,b,c

# Поведение переменных $* и $@, при "пустой" $IFS, зависит
# от версии командной оболочки, Bash или sh.
# Поэтому, было бы неразумным пользоваться этой "фичей" в своих сценариях.


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

exit 0

Прочие специальные переменные

$-

Список флагов, переданных сценарию (командой set). См. Пример 11-13.

Caution

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

$!

PID последнего, запущенного в фоне, процесса

LOG=$0.log

COMMAND1="sleep 100"

echo "Запись в лог всех PID фоновых процессов, запущенных из сценария: $0" >> "$LOG"
# Таким образом возможен мониторинг и удаление процессов по мере необходимости.
echo >> "$LOG"

# Команды записи в лог.

echo -n "PID of \"$COMMAND1\":  " >> "$LOG"
${COMMAND1} &
echo $! >> "$LOG"
# PID процесса "sleep 100":  1506

# Спасибо Jacques Lederer за предложенный пример.


$_

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

Пример 9-9. Переменная "подчеркивание"

#!/bin/bash

echo $_              # /bin/bash
                     # Для запуска сценария был вызван /bin/bash.

du >/dev/null        # Подавление вывода.
echo $_              # du

ls -al >/dev/null    # Подавление вывода.
echo $_              # -al  (последний аргумент)

:
echo $_              # :
$?

Код возврата команды, функции или скрипта (см. Пример 22-3)

$$

PID самого процесса-сценария. Переменная $$ часто используется при генерации "уникальных" имен для временных файлов (см. Пример A-14, Пример 29-6, Пример 12-23 и Пример 11-23). Обычно это проще чем вызов mktemp.

Примечания

[1]

PID текущего процесса хранится в переменной $$.

[2]

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


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 10. Циклы и ветвления

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

10.1. Циклы

Цикл -- это блок команд, который исполняется многократно до тех пор, пока не будет выполнено условие выхода из цикла.

циклы for

for (in)

Это одна из основных разновидностей циклов. И она значительно отличается от аналога в языке C.

for arg in [list]
do
 команда(ы)...
done



Note

На каждом проходе цикла, переменная-аргумент цикла arg последовательно, одно за другим, принимает значения из списка list.

for arg in "$var1" "$var2" "$var3" ... "$varN"
# На первом проходе, $arg = $var1
# На втором проходе, $arg = $var2
# На третьем проходе, $arg = $var3
# ...
# На N-ном проходе, $arg = $varN

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


Элементы списка могут включать в себя шаблонные символы.

Есл ключевое слово do находится в одной строке со словом for, то после списка аргументов (перед do) необходимо ставить точку с запятой.

for arg in [list] ; do



Пример 10-1. Простой цикл for

#!/bin/bash
# Список планет.

for planet in Меркурий Венера Земля Марс Юпитер Сатурн Уран Нептун Плутон
do
  echo $planet
done

echo

# Если 'список аргументов' заключить в кавычки, то он будет восприниматься как единственный аргумент .
for planet in "Меркурий Венера Земля Марс Юпитер Сатурн Уран Нептун Плутон"
do
  echo $planet
done

exit 0
Note

Каждый из элементов [списка] может содержать несколько аргументов. Это бывает полезным при обработке групп параметров. В этом случае, для принудительного разбора каждого из аргументов в списке, необходимо использовать инструкцию set (см. Пример 11-13).

Пример 10-2. Цикл for с двумя параметрами в каждом из элементов списка

#!/bin/bash
# Список планет.

# Имя кажой планеты ассоциировано с расстоянием от планеты до Солнца (млн. миль).

for planet in "Меркурий 36" "Венера 67" "Земля 93"  "Марс 142" "Юпитер 483"
do
  set -- $planet  # Разбиение переменной "planet" на множество аргументов (позиционных параметров).
  # Конструкция "--" предохраняет от неожиданностей, если $planet "пуста" или начинается с символа "-".

  # Если каждый из аргументов потребуется сохранить, поскольку на следующем проходе они будут "забиты" новыми значениями,
  # То можно поместить их в массив,
  #        original_params=("$@")

  echo "$1      в $2,000,000 миль от Солнца"
  #----две табуляции---к параметру $2 добавлены нули
done

# (Спасибо S.C., за разъяснения.)

exit 0

В качестве списка, в цикле for, можно использовать переменную.

Пример 10-3. Fileinfo: обработка списка файлов, находящегося в переменной

#!/bin/bash
# fileinfo.sh

FILES="/usr/sbin/privatepw
/usr/sbin/pwck
/usr/sbin/go500gw
/usr/bin/fakefile
/sbin/mkreiserfs
/sbin/ypbind"     # Список интересующих нас файлов.
                  # В список добавлен фиктивный файл /usr/bin/fakefile.

echo

for file in $FILES
do

  if [ ! -e "$file" ]       # Проверка наличия файла.
  then
    echo "Файл $file не найден."; echo
    continue                # Переход к следующей итерации.
  fi

  ls -l $file | awk '{ print $8 "         размер: " $5 }'  # Печать 2 полей.
  whatis `basename $file`   # Информация о файле.
  echo
done  

exit 0

В [списке] цикла for могут быть использованы имена файлов, которые в свою очередь могут содержать символы-шаблоны.

Пример 10-4. Обработка списка файлов в цикле for

#!/bin/bash
# list-glob.sh: Создание список файлов в цикле for с использованием
# операции подстановки имен файлов ("globbing").

echo

for file in *
do
  ls -l "$file"  # Список всех файлов в $PWD (текущем каталоге).
  # Напоминаю, что символу "*" соответствует любое имя файла,
  # однако, в операциях подстановки имен файлов ("globbing"),
  # имеются исключения -- имена файлов, начинающиеся с точки.

  # Если в каталоге нет ни одного файла, соответствующего шаблону,
  # то за имя файла принимается сам шаблон.
  # Чтобы избежать этого, используйте ключ nullglob
  # (shopt -s nullglob).
  # Спасибо S.C.
done

echo; echo

for file in [jx]*
do
  rm -f $file    # Удаление файлов, начинающихся с "j" или "x" в $PWD.
  echo "Удален файл \"$file\"".
done

echo

exit 0

Если [список] в цикле for не задан, то в качестве оного используется переменная $@ -- список аргументов командной строки. Оень остроумно эта особенность проиллюстрирована в Пример A-18.

Пример 10-5. Цикл for без списка аргументов

#!/bin/bash

# Попробуйте вызвать этот сценарий с аргументами и без них и посмотреть на результаты.

for a
do
 echo -n "$a "
done

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

echo

exit 0

При создании списка аргументов, в цикле for допускается пользоваться подстановкой команд. См. Пример 12-39, Пример 10-10 и Пример 12-33.

Пример 10-6. Создание списка аргументов в цикле for с помощью операции подстановки команд

#!/bin/bash
# уЩЫЬ for гЯ [гаЩгЫЯЭ], гЯкФСЮЮйЭ г аЯЭЯниР аЯФгдСЮЯзЫЩ ЫЯЭСЮФ.

NUMBERS="9 7 3 8 37.53"

for number in `echo $NUMBERS`  # for number in 9 7 3 8 37.53
do
  echo -n "$number "
done

echo 
exit 0

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

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

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

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

E_BADARGS=65
E_NOFILE=66

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

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


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

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


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

exit 0

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

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

#!/bin/bash
# userlist.sh

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

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


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

exit 0

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

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

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

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

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

exit 0

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

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

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

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


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

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

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

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

exit 0

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

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

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

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

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

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

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

exit 0

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

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

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

echo

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

echo; echo

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

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

LIMIT=10

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

echo; echo

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

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

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

echo; echo

exit 0

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

---

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

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

#!/bin/bash

EXPECTED_ARGS=2
E_BADARGS=65

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


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


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

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

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


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

exit 0
while

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

while [condition]
do
 command...
done



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

while [condition] ; do



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

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

#!/bin/bash

var0=0
LIMIT=10

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

echo

exit 0

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

#!/bin/bash

echo

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

exit 0

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

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

#!/bin/bash

var1=unset
previous=$var1

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

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

exit 0

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

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

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

LIMIT=10
a=1

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

echo; echo

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

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

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

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

echo

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

exit 0
Note

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

until

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

until [condition-is-true]
do
 command...
done



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

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

until [condition-is-true] ; do



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

#!/bin/bash

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

exit 0

Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

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

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

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

#!/bin/bash

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


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

Ввод/вывод

echo

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

echo Hello
echo $a


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

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

Note

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

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


Note

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

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

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

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

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

bash$ ls -l /usr/share/apps/kjezz/sounds
-rw-r--r--    1 root     root         1407 Nov  7  2000 reflect.au
-rw-r--r--    1 root     root          362 Nov  7  2000 seconds.au

bash$ echo `ls -l /usr/share/apps/kjezz/sounds`
total 40 -rw-r--r-- 1 root root 716 Nov 7 2000 reflect.au -rw-r--r-- 1 root root 362 Nov 7 2000 seconds.au
             


Note

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

bash$ type -a echo
echo is a shell builtin
echo is /bin/echo
             


printf

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

printf format-string... parameter...

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

Caution

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

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

#!/bin/bash
# printf demo

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

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

echo

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

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

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

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

echo

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

echo

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

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

exit 0

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

E_BADDIR=65

var=nonexistent_directory

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

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

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


read

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

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

#!/bin/bash

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

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

echo "var1 = $var1"


echo

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

exit 0

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

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

#!/bin/bash

echo

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

echo

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

echo

exit 0

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

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

#!/bin/bash

echo

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

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

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

echo; echo

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

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

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

echo 

exit 0

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

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

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

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

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


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

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

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

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

SUCCESS=0
OTHER=65

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

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

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

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

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

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

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


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

exit $OTHER

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

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

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

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

#!/bin/bash

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

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

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

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

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

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

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



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

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

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

exit 0
Note

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

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

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


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

cd

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

(cd /source/directory && tar cf - . ) | (cd /dest/directory && tar xpvf -)
[взято из упоминавшегося ранее примера]

Команда cd с ключом -P (physical) игнорирует символические ссылки.

Команда "cd -" выполняет переход в каталог $OLDPWD -- предыдущий рабочий каталог.

Caution

Неожиданным образом выполняется команда cd, если ей передать, в качестве каталога назначения, два слэша.

bash$ cd //
bash$ pwd
//
             
Само собой разумеется, это должен был бы быть каталог /. Эта проблема наблюдается как в командной строке, так и в сценариях.

pwd

Выводит название текущего рабочего каталога (Print Working Directory) (см. Пример 11-7). Кроме того, имя текущего каталога хранится во внутренней переменной $PWD.

pushd, popd, dirs

Этот набор команд является составной частью механизма "закладок" на каталоги и позволяет перемещаться по каталогам вперед и назад в заданном порядке. Для хранения имен каталогов используется стек (LIFO -- "последний вошел, первый вышел").

pushd dir-name -- помещает имя текущего каталога в стек и осуществляет переход в каталог dir-name.

popd -- выталкивает, находящееся на вершине стека, имя каталога и одновременно осуществляет переход в каталог, оказавшийся на врешине стека.

dirs -- выводит содержимое стека каталогов (сравните с переменной $DIRSTACK). В случае успеха, обе команды -- pushd и popd автоматически вызывают dirs.

Эти команды могут оказаться весьма полезными, когда в сценарии нужно производить частую смену каталогов, но при этом не хочется жестко "зашивать" имена каталогов. Обратите внимание: содержимое стека каталогов постоянно хранится в переменной-массиве -- $DIRSTACK.

Пример 11-7. Смена текущего каталога

#!/bin/bash

dir1=/usr/local
dir2=/var/spool

pushd $dir1
# Команда 'dirs' будет вызвана автоматически (на stdout будет выведено содержимое стека).
echo "Выполнен переход в каталог `pwd`." # Обратные одиночные кавычки.

# Теперь можно выполнить какие либо действия в каталоге 'dir1'.
pushd $dir2
echo "Выполнен переход в каталог `pwd`."

# Теперь можно выполнить какие либо действия в каталоге 'dir2'.
echo "На вершине стека находится: $DIRSTACK."
popd
echo "Возврат в каталог `pwd`."

# Теперь можно выполнить какие либо действия в каталоге 'dir1'.
popd
echo "Возврат в первоначальный рабочий каталог `pwd`."

exit 0

Переменные

let

Команда let производит арифметические операции над переменными. В большинстве случаев, ее можно считать упрощенным вариантом команды expr.

Пример 11-8. Команда let, арифметические операции.

#!/bin/bash

echo

let a=11          # То же, что и 'a=11'
let a=a+5         # Эквивалентно "a = a + 5"
                  # (Двойные кавычки и дополнительные пробелы делают код более удобочитаемым)
echo "11 + 5 = $a"

let "a <<= 3"     # Эквивалентно  let "a = a << 3"
echo "\"\$a\" (=16) после сдвига влево на 3 разряда = $a"

let "a /= 4"      # Эквивалентно let "a = a / 4"
echo "128 / 4 = $a"

let "a -= 5"      # Эквивалентно let "a = a - 5"
echo "32 - 5 = $a"

let "a = a * 10"  # Эквивалентно let "a = a * 10"
echo "27 * 10 = $a"

let "a %= 8"      # Эквивалентно let "a = a % 8"
echo "270 mod 8 = $a  (270 / 8 = 33, остаток = $a)"

echo

exit 0
eval

eval arg1 [arg2] ... [argN]

Транслирует список аргументов, из списка, в команды.

Пример 11-9. Демонстрация команды eval

#!/bin/bash

y=`eval ls -l`  # Подобно y=`ls -l`
echo $y         # но символы перевода строки не выводятся, поскольку имя переменной не в кавычках.
echo
echo "$y"       # Если имя переменной записать в кавычках -- символы перевода строки сохраняются.

echo; echo

y=`eval df`     # Аналогично y=`df`
echo $y         # но без символов перевода строки.

#  Когда производится подавление вывода символов LF (перевод строки), то анализ
#+ результатов различными утилитами, такими как awk, можно сделать проще.

exit 0

Пример 11-10. Принудительное завершение сеанса

#!/bin/bash

y=`eval ps ax | sed -n '/ppp/p' | awk '{ print $1 }'`
# Выяснить PID процесса 'ppp'.

kill -9 $y   # "Прихлопнуть" его

# Предыдущие строки можно заменить одной строкой
#  kill -9 `ps ax | awk '/ppp/ { print $1 }'


chmod 666 /dev/ttyS3
# Завершенный, по сигналу SIGKILL, ppp изменяет права доступа
# к последовательному порту. Вернуть их в первоначальное состояние.

rm /var/lock/LCK..ttyS3   # Удалить lock-файл последовательного порта.

exit 0

Пример 11-11. Шифрование по алгоритму "rot13"

#!/bin/bash
# Реализация алгоритма шифрования "rot13" с помощью 'eval'.
# Сравните со сценарием "rot13.sh".

setvar_rot_13()              # Криптование по алгоритму "rot13"
{
  local varname=$1 varvalue=$2
  eval $varname='$(echo "$varvalue" | tr a-z n-za-m)'
}


setvar_rot_13 var "foobar"   # Пропустить слово "foobar" через rot13.
echo $var                    # sbbone

echo $var | tr a-z n-za-m    # foobar
                             # Расшифровывание.

# Пример предоставил Stephane Chazelas.

exit 0

Rory Winston представил следующий пример, как образец практического использования команды eval.

Пример 11-12. Замена имени переменной на ее значение, в исходном тексте программы на языке Perl, с помощью eval

В программе "test.pl", на языке Perl:
        ...
        my $WEBROOT = <WEBROOT_PATH>;
        ...

Эта попытка подстановки значения переменной вместо ее имени:
        $export WEBROOT_PATH=/usr/local/webroot
        $sed 's/<WEBROOT_PATH>/$WEBROOT_PATH/' < test.pl > out

даст такой результат:
        my $WEBROOT = $WEBROOT_PATH;

Тем не менее:
        $export WEBROOT_PATH=/usr/local/webroot
        $eval sed 's/<WEBROOT_PATH>/$WEBROOT_PATH/' < test.pl > out
#        ====

Этот вариант дал желаемый результат -- имя переменной, в тексте программы,
благополучно было заменено на ее значение:
        my $WEBROOT = /usr/local/webroot
Caution

Команда eval может быть небезопасна. Если существует приемлемая альтернатива, то желательно воздерживаться от использования eval. Так, eval $COMMANDS исполняет код, который записан в переменную COMMANDS, которая, в свою очередь, может содержать весьма неприятные сюрпризы, например rm -rf *. Использование команды eval, для исполнения кода неизвестного происхождения, крайне опасно.

set

Команда set изменяет значения внутренних переменных сценария. Она может использоваться для переключения опций (ключей, флагов), определяющих поведение скрипта. Еще одно применение -- сброс/установка позиционных параметров (аргументов), значения которых будут восприняты как результат работы команды (set `command`).

Пример 11-13. Установка значений аргументов с помощью команды set

#!/bin/bash

# script "set-test"

# Вызовите сценарий с тремя аргументами командной строки,
# например: "./set-test one two three".

echo
echo "Аргументы перед вызовом set \`uname -a\` :"
echo "Аргумент #1 = $1"
echo "Аргумент #2 = $2"
echo "Аргумент #3 = $3"


set `uname -a` # Изменение аргументов
               # значения которых берутся из результата работы `uname -a`

echo $_

echo "Аргументы после вызова set \`uname -a\` :"
#  $1, $2, $3 и т.д. будут переустановлены в соответствии с выводом
#+ команды `uname -a`
echo "Поле #1 'uname -a' = $1"
echo "Поле #2 'uname -a' = $2"
echo "Поле #3 'uname -a' = $3"
echo ---
echo $_        # ---
echo

exit 0

Вызов set без параметров просто выводит список инициализированных переменных окружения.

bash$ set
AUTHORCOPY=/home/bozo/posts
 BASH=/bin/bash
 BASH_VERSION=$'2.05.8(1)-release'
 ...
 XAUTHORITY=/home/bozo/.Xauthority
 _=/etc/bashrc
 variable22=abc
 variable23=xzy
             


Если команда set используется с ключом "--", после которого следует переменная, то значение переменной переносится в позиционные параметры (аргументы). Если имя переменной отсутствует, то эта команда приводит к сбросу позиционных параметров.

Пример 11-14. Изменение значений позиционных параметров (аргументов)

#!/bin/bash

variable="one two three four five"

set -- $variable
# Значения позиционных параметров берутся из "$variable".

first_param=$1
second_param=$2
shift; shift        # сдвиг двух первых параметров.
remaining_params="$*"

echo
echo "первый параметр = $first_param"            # one
echo "второй параметр = $second_param"           # two
echo "остальные параметры = $remaining_params"   # three four five

echo; echo

# Снова.
set -- $variable
first_param=$1
second_param=$2
echo "первый параметр = $first_param"             # one
echo "второй параметр = $second_param"            # two

# ======================================================

set --
# Позиционные параметры сбрасываются, если не задано имя переменной.

first_param=$1
second_param=$2
echo "первый параметр = $first_param"            # (пустое значение)
echo "второй параметр = $second_param"           # (пустое значение)

exit 0

См. так же Пример 10-2 и Пример 12-40.

unset

Команда unset удаляет переменную, фактически -- устанавливает ее значение в null. Обратите внимание: эта команда не может сбрасывать позиционные параметры (аргументы).

bash$ unset PATH

bash$ echo $PATH


bash$


Пример 11-15. "Сброс" переменной

#!/bin/bash
# unset.sh: Сброс переменной.

variable=hello                       # Инициализация.
echo "variable = $variable"

unset variable                       # Сброс.
                                     # Тот же эффект дает   variable=
echo "(unset) variable = $variable"  # $variable = null.

exit 0
export

Команда export экспортирует переменную, делая ее доступной дочерним процессам. К сожалению, невозможно экспортировать переменную родительскому процессу. В качестве примера использования команды export можно привести сценарии инициализации системы, вызываемые в процессе загрузки, которые инициализируют и экспортируют переменные окружения, делая их доступными для пользовательских процессов.

Пример 11-16. Передача переменных во вложенный сценарий awk, с помощью export

#!/bin/bash

# Еще одна версия сценария "column totaler" (col-totaler.sh)
# который суммирует заданную колонку (чисел) в заданном файле.
# Здесь используются переменные окружения, которые передаются сценарию 'awk'.

ARGS=2
E_WRONGARGS=65

if [ $# -ne "$ARGS" ] # Проверка количества входных аргументов.
then
   echo "Порядок использования: `basename $0` filename column-number"
   exit $E_WRONGARGS
fi

filename=$1
column_number=$2

#===== До этой строки идентично первоначальному варианту сценария =====#

export column_number
# Экспорт номера столбца.


# Начало awk-сценария.
# ------------------------------------------------
awk '{ total += $ENVIRON["column_number"]
}
END { print total }' $filename
# ------------------------------------------------
# Конец awk-сценария.


# Спасибо Stephane Chazelas.

exit 0
Tip

Допускается объединение инициализации и экспорта переменной в одну инструкцию: export var1=xxx.

Однако, как заметил Greg Keraunen, в некоторых ситуациях такая комбинация может давать иной результат, нежели раздельная инициализация и экспорт.

bash$ export var=(a b); echo ${var[0]}
(a b)
bash$ var=(a b); export var; echo ${var[0]}
a
             


declare, typeset

Команды declare и typeset задают и/или накладывают ограничения на переменные.

readonly

То же самое, что и declare -r, делает переменную доступной только для чтения, т.е. переменная становится подобна константе. При попытке изменить значение такой переменной выводится сообщение об ошибке. Эта команда может расцениваться как квалификатор типа const в языке C.

getopts

Мощный инструмент, используемый для разбора аргументов, передаваемых сценарию из командной строки. Это встроенная команда Bash, но имеется и ее "внешний" аналог /usr/bin/getopt, а так же программистам, пишущим на C, хорошо знакома похожая библиотечная функция getopt. Она позволяет обрабатывать серии опций, объединенных в один аргумент [2] и дополнительные аргументы, передаваемые сценарию (например, scriptname -abc -e /usr/local).

С командой getopts очень тесно взаимосвязаны скрытые переменные. $OPTIND -- указатель на аргумент (OPTion INDex) и $OPTARG (OPTion ARGument) -- дополнительный аргумент опции. Символ двоеточия, следующий за именем опции, указывает на то, что она имеет дополнительный аргумент.

Обычно getopts упаковывается в цикл while, в каждом проходе цикла извлекается очередная опция и ее аргумент (если он имеется), обрабатывается, затем уменьшается на 1 скрытая переменная $OPTIND и выполняется переход к началу новой итерации.

Note
  1. Опциям (ключам), передаваемым в сценарий из командной строки, должен предшествовать символ "минус" (-) или "плюс" (+). Этот префикс (- или +) позволяет getopts отличать опции (ключи) от прочих аргументов. Фактически, getopts не будет обрабатывать аргументы, если им не предшествует символ - или +, выделение опций будет прекращено как только встретится первый аргумент.

  2. Типичная конструкция цикла while с getopts несколько отличается от стандартной из-за отсутствия квадратных скобок, проверяющих условие продолжения цикла.

  3. Пример getopts, заменившей устаревшую, и не такую мощную, внешнюю команду getopt.



while getopts ":abcde:fg" Option
# Начальное объявление цикла анализа опций.
# a, b, c, d, e, f, g -- это возможные опции (ключи).
# Символ : после опции 'e' указывает на то, что с данной опцией может идти
# дополнительный аргумент.
do
  case $Option in
    a ) # Действия, предусмотренные опцией 'a'.
    b ) # Действия, предусмотренные опцией 'b'.
    ...
    e)  # Действия, предусмотренные опцией 'e', а так же необходимо обработать $OPTARG,
        # в которой находится дополнительный аргумент этой опции.
    ...
    g ) # Действия, предусмотренные опцией 'g'.
  esac
done
shift $(($OPTIND - 1))
# Перейти к следующей опции.

# Все не так сложно, как может показаться ;-)
             


Пример 11-17. Прием опций/аргументов, передаваемых сценарию, с помощью getopts

#!/bin/bash
# ex33.sh

# Обработка опций командной строки с помощью 'getopts'.

# Попробуйте вызвать этот сценарий как:
# 'scriptname -mn'
# 'scriptname -oq qOption' (qOption может быть любой произвольной строкой.)
# 'scriptname -qXXX -r'
#
# 'scriptname -qr'    - Неожиданный результат: "r" будет воспринят как дополнительный аргумент опции "q"
# 'scriptname -q -r'  - То же самое, что и выше
#  Если опция ожидает дополнительный аргумент ("flag:"), то следующий параметр
#  в командной строке, будет воспринят как дополнительный аргумент этой опции.

NO_ARGS=0
E_OPTERROR=65

if [ $# -eq "$NO_ARGS" ]  # Сценарий вызван без аргументов?
then
  echo "Порядок использования: `basename $0` options (-mnopqrs)"
  exit $E_OPTERROR        # Если аргументы отсутствуют -- выход с сообщением
                          # о порядке использования скрипта
fi
# Порядок использования: scriptname -options
# Обратите внимание: дефис (-) обязателен


while getopts ":mnopq:rs" Option
do
echo $OPTIND
  case $Option in
    m     ) echo "Сценарий #1: ключ -m-";;
    n | o ) echo "Сценарий #2: ключ -$Option-";;
    p     ) echo "Сценарий #3: ключ -p-";;
    q     ) echo "Сценарий #4: ключ -q-, с аргументом \"$OPTARG\"";;
    # Обратите внимание: с ключом 'q' должен передаваться дополнительный аргумент,
    # в противном случае отработает выбор "по-умолчанию".
    r | s ) echo "Сценарий #5: ключ -$Option-"'';;
    *     ) echo "Выбран недопустимый ключ.";;   # ПО-УМОЛЧАНИЮ
  esac
done
shift $(($OPTIND - 1))
# Переход к очередному параметру командной строки.

exit 0

Управление сценарием

source, . (точка)

Когда эта команда вызывается из командной строки, то это приводит к запуску указанного сценария. Внутри сценария, команда source file-name загружает файл file-name. Таким образом она очень напоминает директиву препроцессора языка C/C++ -- "#include". Может найти применение в ситуациях, когда несколько сценариев пользуются одним файлом с данными или библиотекой функций.

Пример 11-18. "Подключение" внешнего файла

#!/bin/bash

. data-file    # Загрузка файла с данными.
# Тот же эффект дает "source data-file", но этот вариант более переносим.

#  Файл "data-file" должен находиться в текущем каталоге,
#+ т.к. путь к нему не указан.

# Теперь, выведем некоторые переменные из этого файла.

echo "variable1 (из data-file) = $variable1"
echo "variable3 (из data-file) = $variable3"

let "sum = $variable2 + $variable4"
echo "Сумма variable2 + variable4 (из data-file) = $sum"
echo "message1 (из data-file):  \"$message1\""
# Обратите внимание:             кавычки экранированы

print_message Вызвана функция вывода сообщений, находящаяся в data-file.


exit 0

Файл data-file для Пример 11-18, представленного выше, должен находиться в том же каталоге.

# Этот файл подключается к сценарию.
# Подключаемые файлы могут содержать об"явления переменных, функций и т.п.
# Загружаться может командой 'source' или '.' .

# Инициализация некоторых переменных.

variable1=22
variable2=474
variable3=5
variable4=97

message1="Привет! Как поживаете?"
message2="Досвидания!"

print_message ()
{
# Вывод сообщения переданного в эту функцию.

  if [ -z "$1" ]
  then
    return 1
    # Ошибка, если аргумент отсутствует.
  fi

  echo

  until [ -z "$1" ]
  do
    # Цикл по всем аргументам функции.
    echo -n "$1"
    # Вывод аргумента с подавлением символа перевода строки.
    echo -n " "
    # Вставить пробел, для разделения выводимых аргументов.
    shift
    # Переход к следующему аргументу.
  done  

  echo

  return 0
} 

Сценарий может подключить даже самого себя, только этому едва ли можно найти какое либо практическое применение.

Пример 11-19. Пример (бесполезный) сценария, который подключает себя самого.

#!/bin/bash
# self-source.sh: сценарий, который рекурсивно подключает себя самого."
# Из "Бестолковые трюки", том II.

MAXPASSCNT=100    # Максимальное количество проходов.

echo -n  "$pass_count  "
#  На первом проходе выведет два пробела,
#+ т.к. $pass_count еще не инициализирована.

let "pass_count += 1"
#  Операция инкремента неинициализированной переменной $pass_count
#+ на первом проходе вполне допустима.
#  Этот прием срабатывает в Bash и pdksh, но,
#+ при переносе сценария в другие командные оболочки,
#+ он может оказаться неработоспособным или даже опасным.
#  Лучшим выходом из положения, будет присвоить переменной $pass_count
#+ значение 0, если она неинициализирована.

while [ "$pass_count" -le $MAXPASSCNT ]
do
  . $0   # "Подключение" самого себя.
         # ./$0 (истинная рекурсия) в данной ситуации не сработает.
done  

#  Происходящее здесь фактически не является рекурсией как таковой,
#+ т.к. сценарий как бы "расширяет" себя самого
#+ (добавляя новый блок кода)
#+ на каждом проходе цикла 'while',
#+ командой 'source' в строке 22.
#
#  Само собой разумеется, что первая строка (#!), вновь подключенного сценария,
#+ интерпретируется как комментарий, а не как начало нового сценария (sha-bang)

echo

exit 0   # The net effect is counting from 1 to 100.
         # Very impressive.

# Упражнение:
# ----------
# Напишите сценарий, который использовал бы этот трюк для чего либо полезного.
exit

Безусловное завершение работы сценария. Команде exit можно передать целое число, которое будет возвращено вызывающему процессу как код завершения. Вообще, считается хорошей практикой завершать работу сценария, за исключением простейших случаев, командой exit 0, чтобы проинформировать родительский процесс об успешном завершении.

Note

Если сценарий завершается командой exit без аргументов, то в качестве кода завершения сценария принимается код завершения последней выполненной команды, не считая самой команды exit.

exec

Это встроенная команда интерпретатора shell, заменяет текущий процесс новым процессом, запускаемым командой exec. Обычно, когда командный интерпретатор встречает эту команду, то он порождает дочерний процесс, чтобы исполнить команду. При использовании встроенной команды exec, оболочка не порождает еще один процесс, а заменяет текущий процесс другим. Для сценария это означает его завершение сразу после исполнения команды exec. По этой причине, если вам встретится exec в сценарии, то, скорее всего это будет последняя команда в сценарии.

Пример 11-20. Команда exec

#!/bin/bash

exec echo "Завершение \"$0\"."   # Это завершение работы сценария.

# ----------------------------------
# Следующие ниже строки никогда не будут исполнены
echo "Эта строка никогда не будет выведена на экран."

exit 99                       #  Сценарий завершит работу не здесь.
                              #  Проверьте код завершения сценария
                              #+ командой 'echo $?'.
                              #  Он точно не будет равен 99.

Пример 11-21. Сценарий, который запускает себя самого

#!/bin/bash
# self-exec.sh

echo

echo "Эта строка в сценарии единственная, но она продолжает выводиться раз за разом."
echo "PID остался равным $$."
#     Демонстрация того, что команда exec не порождает дочерний процесс.

echo "==================== Для завершения - нажмите Ctl-C ===================="

sleep 1

exec $0   #  Запуск очередного экземпляра этого же сценария
          #+ который замещает предыдущий.

echo "Эта строка никогда не будет выведена!"  # Почему?

exit 0

Команда exec так же может использоваться для перенаправления. Так, команда exec <zzz-file заменит стандартное устройство ввода (stdin) файлом zzz-file (см. Пример 16-1).

Note

Ключ -exec команды find -- это не то же самое, что встроенная команда exec.

shopt

Эта команда позволяет изменять ключи (опции) оболочки на лету (см. Пример 23-1 и Пример 23-2). Ее часто можно встретить в стартовых файлах, но может использоваться и в обычных сценариях. Требует Bash версии 2 или выше.

shopt -s cdspell
# Исправляет незначительные орфографические ошибки в именах каталогов в команде 'cd'

cd /hpme  # Oops! Имелось ввиду '/home'.
pwd       # /home
          # Shell исправил опечатку.


Команды

true

Команда возвращает код завершения -- ноль, или успешное завершение, и ничего больше.

# Бесконечный цикл
while true   # вместо ":"
do
   operation-1
   operation-2
   ...
   operation-n
   # Следует предусмотреть способ завершения цикла.
done


false

Возвращает код завершения, свидетельствующий о неудаче, и ничего более.

# Цикл, который никогда не будет исполнен
while false
do
   # Следующий код не будет исполнен никогда.
   operation-1
   operation-2
   ...
   operation-n
done  


type [cmd]

Очень похожа на внешнюю команду which, type cmd выводит полный путь к "cmd". В отличие от which, type является внутренней командой Bash. С опцией -a не только различает ключевые слова и внутренние команды, но и определяет местоположение внешних команд с именами, идентичными внутренним.

bash$ type '['
[ is a shell builtin
bash$ type -a '['
[ is a shell builtin
 [ is /usr/bin/[
             


hash [cmds]

Запоминает путь к заданной команде (в хэш-таблице командной оболочки), благодаря чему, при повторном обращении к ней, оболочка или сценарий уже не будет искать путь к команде в $PATH. При вызове команды hash без аргументов, просто выводит содержимое хэш-таблицы. С ключом -r -- очищает хэш-таблицу.

help

help COMMAND -- выводит краткую справку по использованию внутренней команды COMMAND. Аналог команды whatis, только для внутренних команд.

bash$ help exit
exit: exit [n]
    Exit the shell with a status of N.  If N is omitted, the exit status
    is that of the last command executed.
             


11.1. Команды управления заданиями

Некоторые из нижеследующих команд принимают, в качестве аргумента, "идентификатор задания". См. таблицу в конце главы.

jobs

Выводит список заданий, исполняющихся в фоне. Команда ps более информативна.

Note

Задания и процессы легко спутать. Некоторые внутренние команды, такие как kill, disown и wait принимают в качестве параметра либо номер задания, либо номер процесса. Команды fg, bg и jobs принимают только номер задания.

bash$ sleep 100 &
[1] 1384

bash $ jobs
[1]+  Running                 sleep 100 &


"1" -- это номер задания (управление заданиями осуществляет текущий командный интерпретатор), а "1384" -- номер процесса (управление процессами осуществляется системой). Завершить задание/процесс ("прихлопнуть") можно либо командой kill %1, либо kill 1384.

Спасибо S.C.

disown

Удаляет задание из таблицы активных заданий командной оболочки.

fg, bg

Команда fg переводит задание из фона на передний план. Команда bg перезапускает приостановленное задание в фоновом режиме. Если эти команды были вызваны без указания номера задания, то они воздействуют на текущее исполняющееся задание.

wait

Останавливает работу сценария до тех пор пока не будут завершены все фоновые задания или пока не будет завершено задание/процесс с указанным номером задания/PID процесса. Возвращает код завершения указанного задания/процесса.

Вы можете использовать команду wait для предотвращения преждевременного завершения сценария до того, как завершит работу фоновое задание.

Пример 11-22. Ожидание завершения процесса перед тем как продолжить работу

#!/bin/bash

ROOT_UID=0   # Только пользователь с $UID = 0 имеет привилегии root.
E_NOTROOT=65
E_NOPARAMS=66

if [ "$UID" -ne "$ROOT_UID" ]
then
  echo "Для запуска этого сценария вы должны обладать привилегиями root."
  exit $E_NOTROOT
fi

if [ -z "$1" ]
then
  echo "Порядок использования: `basename $0` имя-файла"
  exit $E_NOPARAMS
fi


echo "Обновляется база данных 'locate'..."
echo "Это может занять продолжительное время."
updatedb /usr &     # Должна запускаться с правами root.

wait
# В этом месте сценарий приостанавливает свою работу до тех пор, пока не отработает 'updatedb'.
# Желательно обновить базу данных перед тем как выполнить поиск файла.

locate $1

# В худшем случае, без команды wait, сценарий завершил бы свою работу до того,
# как завершила бы работу утилита 'updatedb',
# сделав из нее "осиротевший" процесс.

exit 0

Команда wait может принимать необязательный параметр -- номер задания/процесса, например, wait %1 или wait $PPID. См. таблицу идентификации заданий.

Tip

При запуске команды в фоне из сценария может возникнуть ситуация, когда сценарий приостанавливает свою работу до тех пор, пока не будет нажата клавиша ENTER. Это, кажется, происходит с командами, делающими вывод на stdout. Такое поведение может вызывать раздражение у пользователя.

#!/bin/bash
# test.sh

ls -l &
echo "Done."
bash$ ./test.sh
Done.
 [bozo@localhost test-scripts]$ total 1
 -rwxr-xr-x    1 bozo     bozo           34 Oct 11 15:09 test.sh
 _
              


Разместив команду wait, после запуска фонового задания, можно предотвратить такое поведение сценария.

#!/bin/bash
# test.sh

ls -l &
echo "Done."
wait
bash$ ./test.sh
Done.
 [bozo@localhost test-scripts]$ total 1
 -rwxr-xr-x    1 bozo     bozo           34 Oct 11 15:09 test.sh
              
Перенаправление вывода в файл или даже на устройство /dev/null также снимает эту проблему.

suspend

Действует аналогично нажатию на комбинацию клавиш Control+-Z, за исключением того, что она приостанавливает работу командной оболочки.

logout

Завершает сеанс работы командной оболочки, можно указать необязательный код завершения.

times

Выдает статистику исполнения команд в единицах системного времени, в следующем виде:

0m0.020s 0m0.020s
Имеет весьма ограниченную сферу применения, так как сценарии крайне редко подвергаются профилированию.

kill

Принудительное завершение процесса путем передачи ему соответствующего сигнала (см. Пример 13-4).

Пример 11-23. Сценарий, завершающий себя сам с помощью команды kill

#!/bin/bash
# self-destruct.sh

kill $$  # Сценарий завершает себя сам.
         # Надеюсь вы еще не забыли, что "$$" -- это PID сценария.

echo "Эта строка никогда не будет выведена."
# Вместо него на stdout будет выведено сообщение "Terminated".

exit 0

#  Какой код завершения вернет сценарий?
#
# sh self-destruct.sh
# echo $?
# 143
#
# 143 = 128 + 15
#             сигнал TERM
Note

Команда kill -l выведет список всех сигналов. Команда kill -9 -- это "жесткий kill", она используется, как правило, для завершения зависших процессов, которые упорно отказываются "умирать", отвергая простой kill. Иногда достаточно подать команду kill -15. "Процессы-зомби", т.е. процессы, "родители" которых уже завершили работу, не могут быть "убиты" таким способом (невозможно "убить" "мертвого"), рано или поздно с ними "расправится" процесс init.

command

Директива command COMMAND запрещает использование псевдонимов и функций с именем "COMMAND".

Note

Это одна из трех директив командного интерпретатора, которая влияет на обработку команд. Другие две -- builtin и enable.

builtin

Конструкция builtin BUILTIN_COMMAND запускает внутреннюю команду "BUILTIN_COMMAND", на время запрещая использование функций и внешних системных команд с тем же именем.

enable

Либо запрещает, либо разрешает вызов внутренних команд. Например, enable -n kill запрещает использование внутренней команды kill, в результате, когда интерпретатор встретит команду kill, то он вызовет внешнюю команду kill, т.е. /bin/kill.

Команда enable -a выведет список всех внутренних команд, указывая для каждой -- действительно ли она разрешена. Команда enable -f filename загрузит внутренние команды как разделяемую библиотеку (DLL) из указанного объектного файла. [3].

autoload

Перенесена в Bash из ksh. Если функция объявлена как autoload, то она будет загружена из внешнего файла в момент первого вызова. [4] Такой прием помогает экономить системные ресурсы.

Обратите внимание: autoload не является частью ядра Bash. Ее необходимо загрузить с помощью команды enable -f (см. выше).

Таблица 11-1. Идентификация заданий

Нотация Описание
%N Номер задания [N]
%S Вызов (командная строка) задания, которая начинается со строки S
%?S Вызов (командная строка) задания, которая содержит строку S
%% "текущее" задание (последнее задание приостановленное на переднем плане или запущенное в фоне)
%+ "текущее" задание (последнее задание приостановленное на переднем плане или запущенное в фоне)
%- Последнее задание
$! Последний фоновый процесс

Примечания

[1]

Исключение из правил -- команда time, которая в официальной документации к Bash называется ключевым словом.

[2]

Опция -- это аргумент, который управляет поведением сценария и может быть либо включен, либо выключен. Аргумент, который объединяет в себе несколько опций (ключей), определяет поведение сценария в соответствии с отдельными опциями, объединенными в данном аргументе..

[3]

Как правило, исходные тексты подобных библиотек, на языке C, располагаются в каталоге /usr/share/doc/bash-?.??/functions.

Обратите внимание: ключ -f команды enable может отсутствовать в некоторых системах.

[4]

Тот же эффект можно получить с помощью typeset -fu.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 12. Внешние команды, программы и утилиты

Благодаря стандартизации набора команд UNIX-систем, сценарии, на языке командной оболочки, могут быть легко перенесены из системы в систему практически без изменений. Мощь сценариев складывется из наборв системных команд и директив командной оболочки с простыми программными конструкциями.

12.1. Базовые команды

Первая команда, с которой сталкиваются новички

ls

Команда вывода "списка" файлов. Многие недооценивают всю мощь этой скромной команды. Например, с ключом -R, рекурсивный обход дерева каталогов, командв ls выводит содержимое каталогов в виде древовидной структуры. Вот еще ряд любопытных ключей (опций) команды ls: -S -- сортировка по размеру файлов, -t -- сортировка по времени последней модификации файла и -i -- выводит список файлов с их inode (см. Пример 12-3).

Пример 12-1. Создание оглавления диска для записи CDR, с помощью команды ls

#!/bin/bash
# burn-cd.sh
# Сценарий, автоматизирующий процесс прожигания CDR.


SPEED=2          # Если ваше "железо" поддерживает более высокую скорость записи -- можете увеличить этот параметр
IMAGEFILE=cdimage.iso
CONTENTSFILE=contents
DEFAULTDIR=/opt  # В этом каталоге находятся файлы, которые будут записаны на CD.
                 # Каталог должен существовать.

# Используется пакет "cdrecord" от Joerg Schilling.
# (http://www.fokus.gmd.de/nthp/employees/schilling/cdrecord.html)

#  Если этот сценарий предполагается запускать с правами обычного пользователя,
#+ то необходимо установить флаг suid на cdrecord
#+ (chmod u+s /usr/bin/cdrecord, эта команда должна быть выполнена root-ом).

if [ -z "$1" ]
then
  IMAGE_DIRECTORY=$DEFAULTDIR
  # Каталог по-умолчанию, если иной каталог не задан из командной строки.
else
    IMAGE_DIRECTORY=$1
fi

# Создать файл "table of contents".
ls -lRF $IMAGE_DIRECTORY > $IMAGE_DIRECTORY/$CONTENTSFILE
# Ключ "l" -- "расширенный" формат вывода списка файлов.
# Ключ "R" -- рекурсивный обход дерева каталогов.
# Ключ "F" -- добавляет дополнительные метки к именам файлов (к именам каталогов добавдяет оконечный символ /).
echo "Создано оглавление."

# Создать iso-образ.
mkisofs -r -o $IMAGFILE $IMAGE_DIRECTORY
echo "Создан iso-образ файловой системы ISO9660 ($IMAGEFILE)."

# "Прожигание" CDR.
cdrecord -v -isosize speed=$SPEED dev=0,0 $IMAGEFILE
echo "Запись диска."
echo "Наберитесь терпения, это может потребовать некоторого времени."

exit 0
cat, tac

cat -- это акроним от concatenate, выводит содержимое списка файлов на stdout. Для объединения файлов в один файл может использоваться в комбинации с операциями перенаправления (> или >>).

cat filename cat file.1 file.2 file.3 > file.123
Ключ -n, команды cat, вставляет порядковые номера строк в выходном файле. Ключ -b -- нумерут только не пустые строки. Ключ -v выводит непечатаемые символы в нотации с символом ^. Ключ -s заменяет несколько пустых строк, идущих подряд, одной пустой строкой.

см. также Пример 12-21 and Пример 12-17.

tac -- выводит содержимое файлов в обратном порядке, от последней строки к первой.

rev

выводит все строки файла задом наперед на stdout. Это не то же самое, что tac. Команда rev сохраняет порядок следования строк, но переворачивает каждую строку задом наперед.

bash$ cat file1.txt
Это строка 1.
 Это строка 2.


bash$ tac file1.txt
Это строка 2.
 Это строка 1.


bash$ rev file1.txt
.1 акортс отЭ
 .2 акортс отЭ
             


cp

Команда копирования файлов. cp file1 file2 скопирует file1 в file2, перезаписав file2 если он уже существовал (см. Пример 12-5).

Tip

С флагами -a и -r, или -R выполняет копирование дерева каталогов.

mv

Команда перемещения файла. Эквивалентна комбинации команд cp и rm. Может использоваться для перемещения большого количества файлов или для переименования каталогов. Примеры использования команды mv вы найдете в Пример 9-17 и Пример A-3.

Note

При использовании в неинтерактивных сценариях, команде mv следует передавать ключ -f, чтобы подавить запрос подтверждения на перемещение.

Если в качестве каталога назначения указан существующий каталог, то перемещаемый каталог становится подкаталогом каталога назначения..

bash$ mv source_directory target_directory

bash$ ls -lF target_directory
total 1
 drwxrwxr-x    2 bozo  bozo      1024 May 28 19:20 source_directory/
             


rm

Удаляет (remove) файл(ы). Ключ -f позволяет удалять даже файлы ТОЛЬКО-ДЛЯ-ЧТЕНИЯ и подавляет запрос подтверждения на удаление.

Warning

С ключом -r, удаляет все файлы в подкаталогах.

rmdir

Удаляет каталог. Удаляемый каталог не должен содержать файлов, включая "скрытые файлы", [1] иначе каталог не будет удален.

mkdir

Создает новый каталог. mkdir -p project/programs/December создает каталог с заданным именем в требуемом каталоге. Ключ -p позволяет создавать промежуточные родительские каталоги.

chmod

Изменяет атрибуты существующего файла (см. Пример 11-10).

chmod +x filename
# Делает файл "filename" доступным для исполнения всем пользователям.

chmod u+s filename
# Устанавливается бит "suid" для "filename".
# В результате, любой пользователь сможет запустить "filename" с привилегиями владельца файла.
# (Это не относится к файлам-сценариям на языке командной оболочки.)


chmod 644 filename
# Выдает право на запись/чтение владельцу файла "filename", и право на чтение
# всем остальным
# (восьмеричное число).


chmod 1777 directory-name
# Выдает право на чтение, запись и исполнение файлов в каталоге,
# дополнительно устанавливает "sticky bit".
# Это означает, что удалять файлы в этом каталоге могут только владельцы файлов,
# владелец каталога и, само собой разумеется, root.


chattr

Изменяет атрибуты файла. Эта команда подобна команде chmod, за исключением синтаксиса вызова, и работает исключительно в файловой системе ext2.

ln

Создает ссылку на существующий файл. Чаще всего используется с ключом -s, что означает символическую, или "мягкую" (symbolic или "soft") ссылку. Позволяет задавать несколько имен одному и тому же файлу и превосходная альтернатива "псевдонимам" (алиасам) (см. Пример 4-6).

ln -s oldfile newfile создает ссылку, с именем newfile, на существующий файл oldfile, .

man, info

Команды доступа к справочным и информационным страницам по системным командам и установленным программам и утилитам. Как правило, страницы info содержат более подробную информацию, чем man.

Примечания

[1]

Скрытыми считаются файлы, имена которых начинаются с точки, например, ~/.Xdefaults. Такие файлы не выводятся простой командой ls, и не могут быть удалены командой rm -rf *. Как правило, скрытыми делаются конфигурационные файлы в домашнем каталоге пользователя.


Advanced Bash-Scripting Guide: Искусство программирования на языке сценариев командной оболочки
Назад Вперед

Глава 13. Команды системного администрирования

Примеры использования большинства этих команд вы найдете в сценариях начальной загрузки и остановки системы, в каталогах /etc/rc.d. Они, обычно, вызываются пользователем root и используются для администрирования системы или восстановления файловой системы. Эти команды должны использоваться с большой осторожностью, так как некоторые из них могут разрушить систему, при неправильном использовании.

Пользователи и группы

users

Выведет список всех зарегистрировавшихся пользователей. Она, до некоторой степени, является эквивалентом команды who -q.

groups

Выводит список групп, в состав которых входит текущий пользователь. Эта команда соответствует внутренней переменной $GROUPS, но выводит названия групп, а не их числовые идентификаторы.

bash$ groups
bozita cdrom cdwriter audio xgrp

bash$ echo $GROUPS
501
chown, chgrp

Команда chown изменяет владельца файла или файлов. Эта команда полезна в случаях, когда root хочет передать монопольное право на файл от одного пользователя другому. Обычный пользователь не в состоянии изменить владельца файла, за исключением своих собственных файлов.

root# chown bozo *.txt

             


Команда chgrp изменяет группу, которой принадлежит файл или файлы. Чтобы изменить группу, вы должны быть владельцем файла (при этом должны входить в состав указываемой группы) или привилегированным пользователем (root).

chgrp --recursive dunderheads *.data
#  Группа "dunderheads" станет владельцем всех файлов "*.data"
#+ во всех подкаталогах текущей директории ($PWD) (благодаря ключу "--recursive").


useradd, userdel

Команда useradd добавляет учетную запись нового пользователя в систему и создает домашний каталог для данного пользователя. Противоположная, по смыслу, команда userdel удаляет учетную запись пользователя из системы. [1] и удалит соответствующие файлы.

Note

Команда adduser является синонимом для useradd и, как правило, является обычной символической ссылкой на useradd.

id

Команда id выводит идентификатор пользователя (реальный и эффективный) и идентификаторы групп, в состав которых входит пользователь. По сути -- выводит содержимое переменных $UID, $EUID и $GROUPS.

bash$ id
uid=501(bozo) gid=501(bozo) groups=501(bozo),22(cdrom),80(cdwriter),81(audio)

bash$ echo $UID
501

См. также Пример 9-5.

who

Выводит список пользователей, работающих в настоящий момент в системе.

bash$ who
bozo  tty1     Apr 27 17:45
 bozo  pts/0    Apr 27 17:46
 bozo  pts/1    Apr 27 17:47
 bozo  pts/2    Apr 27 17:49
             


С ключом -m -- выводит информацию только о текущем пользователе. Если число аргументов, передаваемых команде, равно двум, то это эквивалентно вызову who -m, например who am i или who The Man.

bash$ who -m
localhost.localdomain!bozo  pts/2    Apr 27 17:49
             


whoami -- похожа на who -m, но выводит только имя пользователя.

bash$ whoami
bozo
             


w

Выводит информацию о системе, список пользователей, подключенных к системе и процессы, связанные с пользователями. Это расширенная версия команды who. Вывод от команды w может быть передан по конвейеру команде grep, с целью поиска требуемого пользователя и/или процесса.

bash$ w | grep startx
bozo  tty1     -                 4:22pm  6:41   4.47s  0.45s  startx
logname

Выводит имя текущего пользователя (из файла /var/run/utmp). Это довольно близкий эквивалент команды whoami.

bash$ logname
bozo

bash$ whoami
bozo

Однако...

bash$ su
Password: ......

bash# whoami
root
bash# logname
bozo
su

Команда предназначена для запуска программы или сценария от имени другого пользователя. su rjones -- запускает командную оболочку от имени пользователя rjones. Запуск команды su без параметров означает запуск командной оболочки от имени привилегированного пользователя root. См. Пример A-17.

sudo

Исполняет заданную команду от имени пользователя root (или другого пользователя).

#!/bin/bash

# Доступ к "секретным" файлам.
sudo cp /root/secretfile /home/bozo/secret


Имена пользователей, которым разрешено использовать команду sudo, хранятся в файле /etc/sudoers.

passwd

Устанавливает или изменяет пароль пользователя.

Команда passwd может использоваться в сценариях, но это плохая практика.

#!/bin/bash
#  set-new-password.sh: Плохая идея.
#  Этот сценарий должен запускаться пользователем root,
#+ а еще лучше -- не запускать его вообще.

ROOT_UID=0         # $UID root = 0.
E_WRONG_USER=65    # Не root?

if [ "$UID" -ne "$ROOT_UID" ]
then
  echo; echo "Только root может запускать этот сценарий."; echo
  exit $E_WRONG_USER
else
  echo; echo "Вам не следовало бы запускать этот сценарий."
fi


username=bozo
NEWPASSWORD=security_violation

echo "$NEWPASSWORD" | passwd --stdin "$username"
#  Ключ '--stdin' указывает 'passwd'
#+ получить новый пароль со stdin (или из конвейера).

echo; echo "Пароль пользователя $username изменен!"

# Использование команды 'passwd' в сценариях -- опасно.

exit 0


ac

Выводит время работы пользователей, основываясь на записях в файле /var/log/wtmp. Это одна из утилит пакета GNU acct.

bash$ ac
        total       68.08
last

Выводит информацию о последних входах/выходах пользователей в ситему, основываясь на записях в файле /var/log/wtmp. Эта команда может отображать информацию об удаленных (в смысле -- с удаленного терминала) соединениях.

newgrp

Позволяет сменить активную группу пользователя. Пользователь остается в системе и текущий каталог не изменяется, но права доступа к файлам вычисляются в соответствии с новыми реальным и эффективным идентификаторами группы. Эта команда используется довольно редко, так как пользователь, обычно, является членом нескольких групп.

Терминалы

tty

Выводит имя терминала текущего пользователя. Обратите внимание: каждое отдельное окно xterm считается отдельным терминалом.

bash$ tty
/dev/pts/1
stty

Выводит и/или изменяет настройки терминала. Эта сложная команда используется в сценариях для управления поведением терминала.

Пример 13-1. Установка символа "забоя"

#!/bin/bash
# erase.sh: Использование команды "stty" для смены клавиши "забоя" при чтении ввода.

echo -n "Как Вас зовут? "
read name                      # Попробуйте стереть последние символы при вводе.
                               # Все работает.
echo "Вас зовут $name."

stty erase '#'                 # Теперь, чтобы стереть символ нужно использовать клавишу "#".
echo -n "Как Вас зовут? "
read name                      # Попробуйте стереть последние символы при вводе с помощью "#".
echo "Вас зовут $name."

exit 0

Пример 13-2. невидимый пароль: Отключение эхо-вывода на терминал

#!/bin/bash

echo
echo -n "Введите пароль "
read passwd
echo "Вы ввели пароль: $passwd"
echo -n "Если кто-нибудь в это время заглядывал Вам через плечо, "
echo "то теперь он знает Ваш пароль."

echo && echo  # Две пустых строки через "and list".

stty -echo    # Отключить эхо-вывод.

echo -n "Введите пароль еще раз "
read passwd
echo
echo "Вы ввели пароль: $passwd"
echo

stty echo     # Восстановить эхо-вывод.

exit 0

Перехват нажатия на клавиши с помощью stty.

Пример 13-3.

#!/bin/bash
# keypress.sh: Определение нажатых клавиш.

echo

old_tty_settings=$(stty -g)   # Сохранить прежние настройки.
stty -icanon
Keypress=$(head -c1)          # или $(dd bs=1 count=1 2> /dev/null)
                              # для других, не GNU, систем

echo
echo "Была нажата клавиша \""$Keypress"\"."
echo

stty "$old_tty_settings"      # Восстановить прежние настройки.

# Спасибо, Stephane Chazelas.

exit 0

См. также Пример 9-3.

tset

Выводит или изменяет настройки терминала. Это более слабая версия stty.

bash$ tset -r
Terminal type is xterm-xfree86.
Kill is control-U (^U).
Interrupt is control-C (^C).
             


setserial

Настройка параметров последовательного порта. Эта команда должна запускаться пользователем, обладающим привилегиями root. Эту команду можно встретить в сценариях настройки системы.

# Взято из /etc/pcmcia/serial :

IRQ=`setserial /dev/$DEVICE | sed -e 's/.*IRQ: //'`
setserial /dev/$DEVICE irq 0 ; setserial /dev/$DEVICE irq $IRQ


getty, agetty

Программа getty или agetty запускается процессом init и обслуживает процедуру входа пользователя в систему. Эти команды не используются в сценариях.

mesg

Разрешает или запрещает доступ к терминалу текущего пользователя командой write.

Tip

Наверное это очень неприятно, когда, во время работы над текстовым файлом, в окне терминала, прямо среди текста, вдруг появляется предложение заказать пиццу. Поэтому, при работе в многопользовательской системе, вам наверняка захочется отключить доступ к своему терминалу.

wall

Имя этой команды -- аббревиатура от "write all", т.е., передать сообщение всем пользователям на все терминалы в сети. Это, в первую очередь, инструмет администратора, который можно использовать, например, для оповещения всех пользователей о предстоящей, в ближайшее время, перезагрузке системы (см. Пример 17-2).

bash$ wall System going down for maintenance in 5 minutes!
Broadcast message from bozo (pts/1) Sun Jul  8 13:53:27 2001...

 System going down for maintenance in 5 minutes!
             


Note

Если доступ к терминалу был закрыт командой mesg, то сообщение на этом терминале выводиться не будет.

dmesg

Выводит все сообщения, выдаваемые системой во время загрузки на stdout. Очень полезная утилита для отладочных целей. Вывод dmesg может анализироваться с помощью grep, sed или awk внутри сценария.

bash$ dmesg | grep hda
Kernel command line: ro root=/dev/hda2
 hda: IBM-DLGA-23080, ATA DISK drive
 hda: 6015744 sectors (3080 MB) w/96KiB Cache, CHS=746/128/63
 hda: hda1 hda2 hda3 < hda5 hda6 hda7 > hda4
             


Информационные и статистические утилиты

uname

Выводит на stdout имя системы. С ключом -a, выводит подробную информацию, содержащую имя системы, имя узла (то есть имя, под которым система известна в сети), версию операционной системы, наименование модификации операционной системы, аппаратную архитектуру (см. Пример 12-4).

bash$ uname -a
Linux localhost.localdomain 2.2.15-2.5.0 #1 Sat Feb 5 00:13:43 EST 2000 i686 unknown

bash$ uname -s
Linux
arch

Выводит тип аппаратной платформы компьютерв. Эквивалентна команде uname -m. См. Пример 10-26.

bash$ arch
i686

bash$ uname -m
i686
lastcomm

Выводит информацию, о ранее выполненных командах, из файла /var/account/pacct. Дополнительно могут указываться команда и пользователь. Это одна из утилит пакета GNU acct.

lastlog

Выводит список всех пользователей, с указанием времени последнего входа в систему. Данные берутся из файла /var/log/lastlog.

bash$ lastlog
root          tty1                      Fri Dec  7 18:43:21 -0700 2001
 bin                                     **Never logged in**
 daemon                                  **Never logged in**
 ...
 bozo          tty1                      Sat Dec  8 21:14:29 -0700 2001



bash$ lastlog | grep root
root          tty1                      Fri Dec  7 18:43:21 -0700 2001
             


Caution

Исполнение этой команды будет завершаться неудачей, если пользователь, вызвавший утилиту, не имеет прав на чтение файла /var/log/lastlog.

lsof

Выводит детальный список открытых, в настоящий момент времени, файлов в виде таблицы. В таблице указаны -- владелец файла, размер файла, тип файла, процесс, открывший файл, и многое другое. Само собой разумеется, что вывод команды lsof может быть обработан, в конвейере, с помощью утилит grep и/или awk.

bash$ lsof
COMMAND    PID    USER   FD   TYPE     DEVICE    SIZE     NODE NAME
 init         1    root  mem    REG        3,5   30748    30303 /sbin/init
 init         1    root  mem    REG        3,5   73120     8069 /lib/ld-2.1.3.so
 init         1    root  mem    REG        3,5  931668     8075 /lib/libc-2.1.3.so
 cardmgr    213    root  mem    REG        3,5   36956    30357 /sbin/cardmgr
 ...
             


strace

Диагностическая и отладочная утилита, предназначенная для трассировки системных вызовов и сигналов. В простейшем случае, запускается как: strace COMMAND.

bash$ strace df
execve("/bin/df", ["df"], [/* 45 vars */]) = 0
 uname({sys="Linux", node="bozo.localdomain", ...}) = 0
 brk(0)                                  = 0x804f5e4
 ...
           


Эквивалентна команде truss.

nmap

Сканер сетевых портов. Эта утилита сканирует сервер в поисках открытых портов и сервисов. Это очень важный инструмент, используемый для поиска уязвимостей при настройке системы.

#!/bin/bash

SERVER=$HOST                           # localhost.localdomain (127.0.0.1).
PORT_NUMBER=25                         # порт службы SMTP.

nmap $SERVER | grep -w "$PORT_NUMBER"  # Проверить -- открыт ли данный порт?
#              grep -w -- поиск только целых слов,
#+             так, например, порт 1025 будет пропущен.

exit 0

# 25/tcp     open        smtp


free

Показывает информацию об использовании памяти, в табличной форме. Вывод команды может быть проанализирован с помощью grep, awk или Perl. Команда procinfo тоже выводит эту информацию, среди всего прочего.

bash$ free
                total       used       free     shared    buffers     cached
   Mem:         30504      28624       1880      15820       1608       16376
   -/+ buffers/cache:      10640      19864
   Swap:        68540       3128      65412

Показать размер неиспользуемой памяти RAM:

bash$ free | grep Mem | awk '{ print $4 }'
1880
procinfo

Извлекает и выводит информацию из файловой системы /proc.

bash$ procinfo | grep Bootup
Bootup: Wed Mar 21 15:15:50 2001    Load average: 0.04 0.21 0.34 3/47 6829
lsdev

Список аппаратных устройств в системе.

bash$ lsdev
Device            DMA   IRQ  I/O Ports
 ------------------------------------------------
 cascade             4     2
 dma                          0080-008f
 dma1                         0000-001f
 dma2                         00c0-00df
 fpu                          00f0-00ff
 ide0                     14  01f0-01f7 03f6-03f6
 ...
             


du

Выводит сведения о занимаемом дисковом пространстве в каталоге и вложенных подкаталогах. Если каталог не указан, то по-умолчанию выводятся сведения о текущем каталоге.

bash$ du -ach
1.0k    ./wi.sh
 1.0k    ./tst.sh
 1.0k    ./random.file
 6.0k    .
 6.0k    total
df

Выводит в табличной форме сведения о смонтированных файловых системах.

bash$ df
Filesystem           1k-blocks      Used Available Use% Mounted on
/dev/hda5               273262     92607    166547  36% /
/dev/hda8               222525    123951     87085  59% /home
/dev/hda7              1408796   1075744    261488  80% /usr
stat

Дает подробную информацию о заданном файле (каталоге или файле устройства) или наборе файлов.

bash$ stat test.cru
  File: "test.cru"
   Size: 49970        Allocated Blocks: 100          Filetype: Regular File
   Mode: (0664/-rw-rw-r--)         Uid: (  501/ bozo)  Gid: (  501/ bozo)
 Device:  3,8   Inode: 18185     Links: 1
 Access: Sat Jun  2 16:40:24 2001
 Modify: Sat Jun  2 16:40:24 2001
 Change: Sat Jun  2 16:40:24 2001
             


Если заданный файл отсутствует, то stat вернет сообщение об ошибке.

bash$ stat nonexistent-file
nonexistent-file: No such file or directory
             


vmstat

Выводит информацию о виртуальной памяти.

bash$ vmstat
   procs                      memory    swap          io system         cpu
 r  b  w   swpd   free   buff  cache  si  so    bi    bo   in    cs  us  sy id
 0  0  0      0  11040   2636  38952   0   0    33     7  271    88   8   3 89
           


netstat

Показывает сведения о сетевой подсистеме, такие как: таблицы маршрутизации и активные соединения. Эта утилита получает сведения из /proc/net (Глава 27). См. Пример 27-2.

netstat -r -- эквивалентна команде route.

uptime

Показывает количество времени, прошедшего с момента последней перезагрузки системы.

bash$ uptime
10:28pm  up  1:57,  3 users,  load average: 0.17, 0.34, 0.27
hostname

Выводит имя узла (сетевое имя системы). С помощью этой команды устанавливается сетевое имя системы в сценарии /etc/rc.d/rc.sysinit. Эквивалентна команде uname -n и внутренней переменной $HOSTNAME.

bash$ hostname
localhost.localdomain

bash$ echo $HOSTNAME
localhost.localdomain
hostid

Выводит 32-битный шестнадцатиричный идентификатор системы.

bash$ hostid
7f0100


Note

Эта команда генерирует "уникальный" числовой идентификатор системы. Некоторые программные продукты используют этот идентификатор в процедуре регистрации. К сожалению, при генерации идентификатора, hostid использует только IP адрес системы, переводя его в шестнадцатиричное представление и переставляя местами пары байт.

Обычно, IP адрес системы можно найти в файле /etc/hosts.

bash$ cat /etc/hosts
127.0.0.1               localhost.localdomain localhost


Переставив местами байты, попарно, в начальном адресе 127.0.0.1, мы получим 0.127.1.0, в шестнадцатиричном представлении это будет 007f0100, что в точности совпадает с приведенным выше результатом выполнения hostid. Наверняка можно найти несколько миллионов компьютеров с таким же "уникальным" идентификатором.

sar

Команда sar (system activity report) выводит очень подробную статистику о функционировании операционной системы. Эту команду можно найти в отдельных коммерческих дистрибутивах UNIX-систем. Она, как правило, не входит в базовый комплект пакетов Linux-систем. Она входит в состав пакета sysstat utilities, автор: Sebastien Godard.

bash$ sar
Linux 2.4.7-10 (localhost.localdomain)         12/31/2001

 10:30:01 AM       CPU     %user     %nice   %system     %idle
 10:40:00 AM       all      1.39      0.00      0.77     97.84
 10:50:00 AM       all     76.83      0.00      1.45     21.72
 11:00:00 AM       all      1.32      0.00      0.69     97.99
 11:10:00 AM       all      1.17      0.00      0.30     98.53
 11:20:00 AM       all      0.51      0.00      0.30     99.19
 06:30:00 PM       all    100.00      0.00    100.01      0.00
 Average:          all      1.39      0.00      0.66     97.95
          
readelf

Показывает сведения о заданном бинарном файле формата elf. Входит в состав пакета binutils.

bash$ readelf -h /bin/bash
ELF Header:
   Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
   Class:                             ELF32
   Data:                              2's complement, little endian
   Version:                           1 (current)
   OS/ABI:                            UNIX - System V
   ABI Version:                       0
   Type:                              EXEC (Executable file)
   . . .
size

Команда size [/path/to/binary] выведет информацию о размерах различных сегментов в исполняемых или библиотечных файлах. В основном используется программистами.

bash$ size /bin/bash
   text    data     bss     dec     hex filename
  495971   22496   17392  535859   82d33 /bin/bash
             


Системный журнал

logger

Добавляет в системный журнал (/var/log/messages) сообщение от пользователя. Для добавления сообщения пользователь не должен обладать привилегиями суперпользователя.

logger Experiencing instability in network connection at 23:10, 05/21.
# Теперь попробуйте дать команду 'tail /var/log/messages'.


Встраивая вызов logger в сценарии, вы получаете возможность заносить отладочную информацию в системный журнал /var/log/messages.

logger -t $0 -i Logging at line "$LINENO".
# Ключ "-t" задает тэг записи в журнале.
# Ключ "-i" -- записывает ID процесса.

# tail /var/log/message
# ...
# Jul  7 20:48:58 localhost ./test.sh[1712]: Logging at line 3.


logrotate

Эта утилита производит манипуляции над системным журналом: ротация, сжатие, удаление и/или отправляет его по электронной почте, по мере необходимости. Как правило, утилита logrotate вызывается демоном crond ежедневно.

Добавляя соответствующие строки в /etc/logrotate.conf, можно заставить logrotate обрабатывать не только системный журнал, но и ваш личный.

Управление заданиями

ps

Process Statistics: Список исполняющихся в данный момент процессов. Обычно вызывается с ключами ax, вывод команды может быть обработан командами grep или sed, с целью поиска требуемого процесса (см. Пример 11-10 и Пример 27-1).

bash$ ps ax | grep sendmail
295 ?     S      0:00 sendmail: accepting connections on port 25
pstree

Список исполняющихся процессов в виде "дерева". С ключом -p -- вместе с именами процессов отображает их PID.

top

Выводит список наиболее активных процессов. С ключом -b -- отображение ведется в обычном текстовом режиме, что дает возможность анализа вывода от команды внутри сценария.

bash$ top -b
  8:30pm  up 3 min,  3 users,  load average: 0.49, 0.32, 0.13
 45 processes: 44 sleeping, 1 running, 0 zombie, 0 stopped
 CPU states: 13.6% user,  7.3% system,  0.0% nice, 78.9% idle
 Mem:    78396K av,   65468K used,   12928K free,       0K shrd,    2352K buff
 Swap:  157208K av,       0K used,  157208K free                   37244K cached

   PID USER     PRI  NI  SIZE  RSS SHARE STAT %CPU %MEM   TIME COMMAND
   848 bozo      17   0   996  996   800 R     5.6  1.2   0:00 top
     1 root       8   0   512  512   444 S     0.0  0.6   0:04 init
     2 root       9   0     0    0     0 SW    0.0  0.0   0:00 keventd
   ...
             


nice

Запускает фоновый процесс с заданным приоритетом. Приоритеты могут задаваться числом из диапазона от 19 (низший приоритет) до -20 (высший приоритет). Но только root может указать значение приоритета меньше нуля (отрицательные значения). См. так же команды renice, snice и skill.

nohup

Запуск команд в режиме игнорирования сигналов прерывания и завершения, что предотвращает завершение работы команды даже если пользователь, запустивший ее, вышел из системы. Если после команды не указан символ &, то она будет исполняться как процесс "переднего плана". Если вы собираетесь использовать nohup в сценариях, то вам потребуется использовать его в связке с командой wait, чтобы не породить процесс "зомби".

pidof

Возвращает идентификатор процесса (pid) по его имени. Поскольку многие команды управления процессами, такие как kill и renice, требуют указать pid процесса, а не его имя, то pidof может сослужить неплохую службу при идентификации процесса по его имени. Эта коменда может рассматриваться как приблизительный эквивалент внутренней переменной $PPID.

bash$ pidof xclock
880
             


Пример 13-4. Использование команды pidof при остановке процесса

#!/bin/bash
# kill-process.sh

NOPROCESS=2

process=xxxyyyzzz  # Несуществующий процесс.
# Только в демонстрационных целях...
# ... чтобы не уничтожить этим сценарием какой-нибудь процесс.
#
# Если с помощью этого сценария вы задумаете разрыватть связь с Internet, то
#     process=pppd

t=`pidof $process`       # Поиск pid (process id) процесса $process.
# pid требует команда 'kill' (невозможно остановить процесс, указав его имя).

if [ -z