Advanced Bash-Scripting Guide — различия между версиями

Материал из VAAL-WIKI
Перейти к: навигация, поиск
м
м
 
(не показано 5 промежуточных версии этого же участника)
Строка 16: Строка 16:
 
Shell-скрипты очень хорошо подходят для быстрого создания прототипов сложных приложений, даже не смотря на ограниченный набор языковых конструкций и определенную "медлительность". Такой метод позволяет детально проработать структуру будущего приложения, обнаружить возможные"ловушки" и лишь затем приступить к кодированию на C, C++, Java, или Perl.
 
Shell-скрипты очень хорошо подходят для быстрого создания прототипов сложных приложений, даже не смотря на ограниченный набор языковых конструкций и определенную "медлительность". Такой метод позволяет детально проработать структуру будущего приложения, обнаружить возможные"ловушки" и лишь затем приступить к кодированию на C, C++, Java, или Perl.
 
<br />
 
<br />
<br />Скрипты возвращают нас к классической философии Unix — "разделяй и властвуй" т.е. разделение сложного проекта на ряд простых подзадач. Многие считают такой подход наилучшим или, по меньшей мере, наиболее эстетичным способом решения возникающих проблем, нежели использование нового поколения языков — "всё-в-одном", таких как Perl.
+
<br />Скрипты возвращают нас к классической философии Unix — "разделяй и властвуй" т.е. разделение сложного проекта на ряд простых подзадач. Многие считают такой подход наилучшим или, по меньшей мере, наиболее эстетичным способом решения возникающих проблем, нежели использование нового поколения языков — "всё-в-одном", таких как Perl.<br />
 
<span style="color:red">'''''Для каких задач неприменимы скрипты'''''</span>:
 
<span style="color:red">'''''Для каких задач неприменимы скрипты'''''</span>:
* для ресурсоемких задач, особенно когда важна скорость исполнения (поиск, сортировка и т.п.)
+
* <span style="color:blue">для ресурсоемких задач, особенно когда важна скорость исполнения (поиск, сортировка и т.п.)</span>
* для задач, связанных с выполнением математических вычислений, особенно это касается вычислений с плавающей запятой, вычислений с повышенной точностью, комплексных чисел(для таких задач лучше использовать C++ или FORTRAN)
+
* <span style="color:blue">для задач, связанных с выполнением математических вычислений, особенно это касается вычислений с плавающей запятой, вычислений с повышенной точностью, комплексных чисел(для таких задач лучше использовать C++ или FORTRAN)</span>
* для кросс-платформенного программирования (для этого лучше подходит язык C)
+
* <span style="color:blue">для кросс-платформенного программирования (для этого лучше подходит язык C)</span>
* для сложных приложений, когда структурирование является жизненной необходимостью(контроль за типами переменных, прототипами функций и т.п.)
+
* <span style="color:blue">для сложных приложений, когда структурирование является жизненной необходимостью(контроль за типами переменных, прототипами функций и т.п.)</span>
* для целевых задач, от которых может зависеть успех предприятия
+
* <span style="color:blue">для целевых задач, от которых может зависеть успех предприятия</span>
* когда во главу угла поставлена безопасность системы, когда необходимо обеспечить целостность системы и защитить её от вторжения, взлома и вандализма
+
* <span style="color:blue">когда во главу угла поставлена безопасность системы, когда необходимо обеспечить целостность системы и защитить её от вторжения, взлома и вандализма</span>
* для проектов, содержащих компоненты, очень тесно взаимодействующие между собой
+
* <span style="color:blue">для проектов, содержащих компоненты, очень тесно взаимодействующие между собой</span>
* для задач, выполняющих огромный объем работ с файлами
+
* <span style="color:blue">для задач, выполняющих огромный объем работ с файлами</span>
* для задач, работающих с многомерными массивами
+
* <span style="color:blue">для задач, работающих с многомерными массивами</span>
* когда необходимо работать со структурами данных, такими как связанные списки или деревья
+
* <span style="color:blue">когда необходимо работать со структурами данных, такими как связанные списки или деревья</span>
* когда необходимо предоставить графический интерфейс с пользователем (GUI)
+
* <span style="color:blue">когда необходимо предоставить графический интерфейс с пользователем (GUI)</span>
* когда необходим прямой доступ к аппаратуре компьютера
+
* <span style="color:blue">когда необходим прямой доступ к аппаратуре компьютера</span>
* когда необходимо выполнять обмен через порты ввода-вывода или сокеты
+
* <span style="color:blue">когда необходимо выполнять обмен через порты ввода-вывода или сокеты</span>
* когда необходимо использовать внешние библиотеки
+
* <span style="color:blue">когда необходимо использовать внешние библиотеки</span>
* для проприетарных, "закрытых" программ (скрипты представляют из себя исходные тексты программ, доступные для всеобщего обозрения)
+
* <span style="color:blue">для проприетарных, "закрытых" программ (скрипты представляют из себя исходные тексты программ, доступные для всеобщего обозрения)</span>
 
<br />
 
<br />
Если выполняется хотя бы одно из вышеперечисленных условий, то вам лучше обратиться к более мощным скриптовым языкам программирования, например Perl, Tcl, Python, Ruby или к высокоуровневым компилирующим языкам — C, C++ или Java. Но даже в этом случае, создание прототипа приложения на языке shell может существенно облегчить разработку.
+
Если <span style="color:red">выполняется хотя бы одно</span> из вышеперечисленных условий, то вам лучше обратиться к более мощным скриптовым языкам программирования, например '''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 позаимствовал некоторые особенности,
 
Название BASH — это аббревиатура от "Bourne-Again Shell" и игра слов от, ставшего уже классикой,"Bourne Shell" Стефена Бурна (Stephen Bourne). В последние годы BASH достиг такой популярности,что стал стандартной командной оболочкой de facto для многих разновидностей Unix. Большинство принципов программирования на BASH одинаково хорошо применимы и в других командных оболочках, таких как Korn Shell (ksh), от которой Bash позаимствовал некоторые особенности,
 
и CShell и его производных(Примечательно, что C Shell не рекомендуется к использованию из-за отдельных проблем, отмеченных Томом Кристиансеном (Tom Christiansen) в октябре 1993 года на Usenet post).  
 
и CShell и его производных(Примечательно, что C Shell не рекомендуется к использованию из-за отдельных проблем, отмеченных Томом Кристиансеном (Tom Christiansen) в октябре 1993 года на Usenet post).  
Строка 41: Строка 41:
 
<br />
 
<br />
 
<br />Не забудьте выдать этим файлам право на исполнение (chmod u+rx scriptname.sh), после чего сценарии можно будет запустить на исполнение и проверить результат их работы. Вам следует помнить, что описание некоторых примеров следует после исходного кода этого примера, поэтому, прежде чем запустить сценарий у себя — ознакомьтесь с его описанием. Скрипты были написаны автором книги, если не оговаривается иное.
 
<br />Не забудьте выдать этим файлам право на исполнение (chmod u+rx scriptname.sh), после чего сценарии можно будет запустить на исполнение и проверить результат их работы. Вам следует помнить, что описание некоторых примеров следует после исходного кода этого примера, поэтому, прежде чем запустить сценарий у себя — ознакомьтесь с его описанием. Скрипты были написаны автором книги, если не оговаривается иное.
 +
 
== Для начала о Sha-Bang ==
 
== Для начала о Sha-Bang ==
 
<br />В простейшем случае, скрипт — это ни что иное, как простой список команд системы, записанный в файл. Создание скриптов поможет сохранить ваше время и силы, которые тратятся на ввод последовательности команд всякий раз, когда необходимо их выполнить.
 
<br />В простейшем случае, скрипт — это ни что иное, как простой список команд системы, записанный в файл. Создание скриптов поможет сохранить ваше время и силы, которые тратятся на ввод последовательности команд всякий раз, когда необходимо их выполнить.
Строка 1624: Строка 1625:
 
* <big>-S</big> — файл является сокетом
 
* <big>-S</big> — файл является сокетом
 
* <big>-t</big> — файл (дескриптор) связан с терминальным устройством
 
* <big>-t</big> — файл (дескриптор) связан с терминальным устройством
Этот ключ может использоваться для проверки — является ли файл стандартным устройством ввода ''stdin'' ([ -t 0 ]) или стандартным устройством вывода ''stdout'' ([ -t 1 ]).  
+
Этот ключ может использоваться для проверки — является ли файл стандартным устройством ввода ''stdin'' ([ -t 0 ]) или стандартным устройством вывода ''stdout'' ([ -t 1 ]).  
 
* <big>-r</big> — файл доступен для чтения (пользователю, запустившему сценарий)
 
* <big>-r</big> — файл доступен для чтения (пользователю, запустившему сценарий)
 
* <big>-w</big> — файл доступен для записи (пользователю, запустившему сценарий)
 
* <big>-w</big> — файл доступен для записи (пользователю, запустившему сценарий)
 
* <big>-x</big> — файл доступен для исполнения (пользователю, запустившему сценарий)
 
* <big>-x</big> — файл доступен для исполнения (пользователю, запустившему сценарий)
 
* <big>-g</big> — set-group-id (sgid) флаг для файла или каталога установлен
 
* <big>-g</big> — set-group-id (sgid) флаг для файла или каталога установлен
Если для каталога установлен флаг sgid, то файлы, создаваемые в таком каталоге, наследуют идентификатор группы каталога, который может не совпадать с идентификатором
+
Если для каталога установлен флаг sgid, то файлы, создаваемые в таком каталоге, наследуют идентификатор группы каталога, который может не совпадать с идентификатором группы, к которой принадлежит пользователь, создавший файл. Это может быть полезно для каталогов, в которых хранятся файлы, общедоступные для группы пользователей.
группы, к которой принадлежит пользователь, создавший файл. Это может быть полезно для каталогов, в которых хранятся файлы, общедоступные для группы пользователей.
+
 
* <big>-u</big> — set-user-id (suid) флаг для файла установлен
 
* <big>-u</big> — set-user-id (suid) флаг для файла установлен
Установленный флаг suid приводит к изменению привилегий запущенного процесса на привилегии владельца исполняемого файла. Исполняемые файлы, владельцем которых
+
Установленный флаг suid приводит к изменению привилегий запущенного процесса на привилегии владельца исполняемого файла. Исполняемые файлы, владельцем которых является root, с установленным флагом set-user-id запускаются с привилегиями root, даже если их запускает обычный пользователь.
является root, с установленным флагом set-user-id запускаются с привилегиями root, даже если их запускает обычный пользователь.
+
Это может оказаться полезным для некоторых программ(таких как ''pppd'' и ''cdrecord''), которые осуществляют доступ к аппаратной части компьютера. В случае отсутствия флага suid, программы не смогут быть запущены рядовым пользователем, не обладающим привилегиями root.
Это может оказаться полезным для некоторых программ(таких как ''pppd'' и ''cdrecord''), которые осуществляют доступ к аппаратной части компьютера. В случае отсутствия
+
флага suid, программы не смогут быть запущены рядовым пользователем, не обладающим привилегиями root.
+
 
  ''-rwsr-xr-t 1 root 178236 Oct 2 2000 /usr/sbin/pppd''
 
  ''-rwsr-xr-t 1 root 178236 Oct 2 2000 /usr/sbin/pppd''
Файл с установленным флагом suid отображается с включенным флагом ''s'' в поле прав доступа.
+
Файл с установленным флагом suid отображается с включенным флагом ''s'' в поле прав доступа.
 
* <big>-k</big> — флаг sticky bit (бит фиксации) установлен
 
* <big>-k</big> — флаг sticky bit (бит фиксации) установлен
Общеизвестно, что флаг "sticky bit" — это специальный тип прав доступа к файлам. Программы сустановленным флагом "sticky bit" остаются в системном кэше после своего
+
Общеизвестно, что флаг "sticky bit" — это специальный тип прав доступа к файлам. Программы сустановленным флагом "sticky bit" остаются в системном кэше после своего завершения,обеспечивая тем самым более быстрый запуск программы. Если флаг установлен для каталога, то это приводит к ограничению прав на запись. Установленный флаг "sticky bit" отображается в виде символа ''t'' в поле прав доступа.
завершения,обеспечивая тем самым более быстрый запуск программы. Если флаг установлен для каталога, то это приводит к ограничению прав на запись. Установленный флаг
+
"sticky bit" отображается в виде символа ''t'' в поле прав доступа.
+
 
  ''drwxrwxrwt 7 root 1024 May 19 21:26 tmp/''
 
  ''drwxrwxrwt 7 root 1024 May 19 21:26 tmp/''
Если пользователь не является владельцем каталога , с установленным "sticky bit", но имеет право на запись в каталог, то он может удалять только те файлы в каталоге,
+
Если пользователь не является владельцем каталога , с установленным "sticky bit", но имеет право на запись в каталог, то он может удалять только те файлы в каталоге, владельцем которых он является. Это предотвращает удаление и перезапись "чужих" файлов в общедоступных каталогах, таких как /tmp.
владельцем которых он является. Это предотвращает удаление и перезапись "чужих" файлов в общедоступных каталогах, таких как /tmp.
+
 
* <big>-O</big> — вы являетесь владельцем файла
 
* <big>-O</big> — вы являетесь владельцем файла
 
* <big>-G</big> — вы принадлежите к той же группе, что и файл
 
* <big>-G</big> — вы принадлежите к той же группе, что и файл
Строка 1753: Строка 1748:
 
* <big>-z</big> — строка "пустая", т.е. имеет нулевую длину
 
* <big>-z</big> — строка "пустая", т.е. имеет нулевую длину
 
* <big>-n</big> — строка не "пустая"
 
* <big>-n</big> — строка не "пустая"
Оператор ''-n'' требует, чтобы строка была заключена в кавычки внутри квадратных скобок. Как правило, проверка строк, не заключенных в кавычки, оператором ''! -z'',  
+
Оператор ''-n'' требует, чтобы строка была заключена в кавычки внутри квадратных скобок. Как правило, проверка строк, не заключенных в кавычки, оператором ''! -z'', или просто указание строки без кавычек внутри квадратных скобок, проходит нормально, однако это небезопасная, с точки зрения отказоустойчивости, практика. Всегда заключайте проверяемую строку в кавычки.
или просто указание строки без кавычек внутри квадратных скобок, проходит нормально, однако это небезопасная, с точки зрения отказоустойчивости,  
+
практика. Всегда заключайте проверяемую строку в кавычки.
+
 
<p style="text-align:center"><span style="color:green"> '''Пример'''</span>: <span style="color:red">'''''Операции сравнения.'''''</span></p>
 
<p style="text-align:center"><span style="color:green"> '''Пример'''</span>: <span style="color:red">'''''Операции сравнения.'''''</span></p>
 
<source lang="bash">
 
<source lang="bash">
Строка 3830: Строка 3823:
 
  randomBetween ${max} ${min} ${divisibleBy}
 
  randomBetween ${max} ${min} ${divisibleBy}
 
# Вывод сообщения об ошибке, если функция вернула некорректное значение.
 
# Вывод сообщения об ошибке, если функция вернула некорректное значение.
  [ ${randomBetweenAnswer} -lt ${min} -o ${randomBetweenAnswer} -gt ${max} ] && echo Выход за границы диапазона MIN .. MAX - ${randomBetweenAnswer}!
+
  [ ${randomBetweenAnswer} -lt ${min} -o ${randomBetweenAnswer} -gt ${max} ] && echo \
 +
"Выход за границы диапазона MIN..MAX" - ${randomBetweenAnswer}!
 
  [ $((randomBetweenAnswer%${divisibleBy})) -ne 0 ] && echo Число не делится на заданный делитель без остатка - ${randomBetweenAnswer}!
 
  [ $((randomBetweenAnswer%${divisibleBy})) -ne 0 ] && echo Число не делится на заданный делитель без остатка - ${randomBetweenAnswer}!
 
# Записать полученное число в массив.
 
# Записать полученное число в массив.
Строка 3837: Строка 3831:
 
# Проверим полученные результаты
 
# Проверим полученные результаты
 
for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
 
for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
  [ ${answer[i+displacement]} -eq 0 ] && echo "Число $i не было получено ни разу." || echo "Число ${i} встречено ${answer[i+displacement]} раз."
+
  [ ${answer[i+displacement]} -eq 0 ] && echo "Число $i не было получено ни разу." || echo \
 +
"Число ${i} встречено ${answer[i+displacement]} раз."
 
done
 
done
 
exit 0
 
exit 0
Строка 3969: Строка 3964:
 
</source>
 
</source>
 
Кроме того, команда [[date]] так же может использоваться для генерации ''псевдослучайных целых чисел''.
 
Кроме того, команда [[date]] так же может использоваться для генерации ''псевдослучайных целых чисел''.
 +
Также при необходимости сгенерировать несколько целых чисел в shell-скрипте можно воспользоваться командой <span style="color:blue">shuf</span>.
 +
Следующая команда сгенерирует одно целое число в диапазоне от 100000 до 999999:
 +
<source lang="bash">
 +
shuf -i 100000-999999 -n 1
 +
</source>
 +
После ключа '''-n''' может быть любое положительное число, задающее количество(!) генерируемых чисел.
  
 
=== Двойные круглые скобки ===
 
=== Двойные круглые скобки ===
Строка 19 332: Строка 19 333:
 
''переданных из командной строки''
 
''переданных из командной строки''
 
|-
 
|-
| style="text-align:center;" | '''$?''' || ''Возвращаемое значение''
+
| style="text-align:center;" | '''$?''' || ''Возвращаемое значение<br />(Код возврата последней команды)''
 
|-
 
|-
 
| style="text-align:center;" | '''$$''' || ''Идентификатор процесса — Process ID (PID) сценария''
 
| style="text-align:center;" | '''$$''' || ''Идентификатор процесса — Process ID (PID) сценария''

Текущая версия на 08:59, 13 февраля 2018

bash (от англ. Bourne again shell, каламбур «Born again» shell — «возрождённый» shell) — усовершенствованная и модернизированная вариация командной оболочки Bourne shell. Одна из наиболее популярных современных разновидностей командной оболочки UNIX. Особенно популярна в среде Linux, где она часто используется в качестве предустановленной командной оболочки.

Bash highway to shell.jpg

Bash — это командный процессор, работающий, как правило, в интерактивном режиме в текстовом окне. Bash также может читать команды из файла, который называется скриптом (или сценарием). Как и все Unix-оболочки, он поддерживает автодополнение имён файлов и директорий, подстановку вывода результата команд, переменные, контроль за порядком выполнения, операторы ветвления и цикла. Ключевые слова, синтаксис и другие основные особенности языка были заимствованы из sh. Другие функции, например, история, были скопированы из csh и ksh. Bash в основном удовлетворяет стандарту POSIX, но с рядом расширений.

Название «bash» является акронимом от Bourne-again-shell («ещё-одна-командная-оболочка-Борна») и представляет собой игру слов: Bourne-shell — одна из популярных разновидностей командной оболочки для UNIX (sh), автором которой является Стивен Борн (1978), усовершенствована в 1987 году Брайаном Фоксом. Фамилия Bourne (Борн) перекликается с английским словом born, означающим «родившийся», отсюда: рождённая-вновь-командная оболочка.

В сентябре 2014 года в bash была обнаружена широко эксплуатируемая уязвимость Bashdoor.

Данный документ является копией электронной версии Advanced-Bash-Scripting-Guide

Содержание

Часть первая. Введение

Shell — это командная оболочка. Но это не просто промежуточное звено между пользователем и операционной системой, это ещё и мощный язык программирования. Программы на языке shell называют сценариями, или скриптами. Фактически, из скриптов доступен полный набор команд, утилит и программ Unix. Если этого недостаточно, то к вашим услугам внутренние команды shell — условные операторы, операторы циклов и пр., которые увеличивают мощь и гибкость сценариев. Shell-скрипты исключительно хороши при программировании задач администрирования системы и др., которые не требуют для своего создания полновесных языков программирования.

Зачем необходимо знание языка Shell?


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

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

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

  • для ресурсоемких задач, особенно когда важна скорость исполнения (поиск, сортировка и т.п.)
  • для задач, связанных с выполнением математических вычислений, особенно это касается вычислений с плавающей запятой, вычислений с повышенной точностью, комплексных чисел(для таких задач лучше использовать C++ или FORTRAN)
  • для кросс-платформенного программирования (для этого лучше подходит язык C)
  • для сложных приложений, когда структурирование является жизненной необходимостью(контроль за типами переменных, прототипами функций и т.п.)
  • для целевых задач, от которых может зависеть успех предприятия
  • когда во главу угла поставлена безопасность системы, когда необходимо обеспечить целостность системы и защитить её от вторжения, взлома и вандализма
  • для проектов, содержащих компоненты, очень тесно взаимодействующие между собой
  • для задач, выполняющих огромный объем работ с файлами
  • для задач, работающих с многомерными массивами
  • когда необходимо работать со структурами данных, такими как связанные списки или деревья
  • когда необходимо предоставить графический интерфейс с пользователем (GUI)
  • когда необходим прямой доступ к аппаратуре компьютера
  • когда необходимо выполнять обмен через порты ввода-вывода или сокеты
  • когда необходимо использовать внешние библиотеки
  • для проприетарных, "закрытых" программ (скрипты представляют из себя исходные тексты программ, доступные для всеобщего обозрения)


Если выполняется хотя бы одно из вышеперечисленных условий, то вам лучше обратиться к более мощным скриптовым языкам программирования, например 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 позаимствовал некоторые особенности, и CShell и его производных(Примечательно, что C Shell не рекомендуется к использованию из-за отдельных проблем, отмеченных Томом Кристиансеном (Tom Christiansen) в октябре 1993 года на Usenet post).

Далее, в тексте документа вы найдете большое количество примеров скриптов, иллюстрирующихвозможности shell. Все примеры -- работающие. Они были протестированы, причем некоторые из них могут пригодиться в повседневной работе. Уважаемый читатель может "поиграть" с рабочим кодом скриптов, сохраняя их в файлы, с именами scriptname.sh.

Не забудьте выдать этим файлам право на исполнение (chmod u+rx scriptname.sh), после чего сценарии можно будет запустить на исполнение и проверить результат их работы. Вам следует помнить, что описание некоторых примеров следует после исходного кода этого примера, поэтому, прежде чем запустить сценарий у себя — ознакомьтесь с его описанием. Скрипты были написаны автором книги, если не оговаривается иное.

Для начала о Sha-Bang


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

Пример. cleanup: Сценарий очистки лог-файлов в /var/log

 # cleanup
 # Для работы сценария требуются права root.
 cd /var/log
 cat /dev/null > messages
 cat /dev/null > wtmp
 echo "Лог-файлы очищены."

Пример. 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" ]; then 
# или if [ "$PWD" != "$LOG_DIR" ]
# не в /var/log?
 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, то это указывает системе какой интерпретатор следует использовать для исполнения сценария. Это двухбайтовая последовательность, или — специальный маркер, определяющий тип сценария, в данном случае — сценарий командной оболочки (см. man magic). Более точно, sha-bang определяет интерпретатор, который вызывается для исполнения сценария, это может быть командная оболочка(shell), иной интерпретатор или утилита:

#!/bin/bash
#!/usr/bin/perl
#!/usr/bin/tcl
#!/bin/sed -f
#!/usr/awk -f


Каждая, из приведенных выше сигнатур, приводит к вызову различных интерпретаторов, будь то /bin/sh — командный интерпретатор по-умолчанию (bash для Linux-систем), либо иной. При переносе сценариев с сигнатурой #!/bin/sh на другие Unix системы, где в качестве командного интерпретатора задан другой shell, вы можете лишиться некоторых особенностей, присущих bash. Поэтому такие сценарии должны быть POSIX совместимыми.
Обратите внимание на то, что сигнатура должна указывать правильный путь к интерпретатору, впротивном случае вы получите сообщение об ошибке -- как правило это "Command not found". Сигнатура #! может быть опущена, если вы не используете специфичных команд. Во втором примере(см. выше) использование сигнатуры #! обязательно, поскольку сценарий использует специфичную конструкцию присваивания значения переменной lines=50. Ещё раз замечу, что сигнатура #!/bin/sh вызывает командный интерпретатор по-умолчанию — /bin/bash в Linux-системах.
В данном руководстве приветствуется модульный подход к построению сценариев. Записывайте, собирайте свою коллекцию участков кода, который может вам встретиться. В конечном итоге вы соберете свою "библиотеку" подпрограмм, которые затем сможете использовать при написании своих сценариев. Например, следующий отрывок сценария проверяет количество аргументов в командной строке:

if [ $# -ne Number_of_expected_args ]; then 
 echo "Usage: `basename $0` whatever" 
 exit $WRONG_ARGS
fi

Запуск сценария

Запустить сценарий можно командой sh scriptname или bash scriptname. (Не рекомендуется запуск сценария командой sh < scriptname, поскольку это запрещает использование устройства стандартного ввода stdin в скрипте). Более удобный вариант — сделать файл скрипта исполняемым, командой chmod.
Это:

chmod 555 scriptname (выдача прав на чтение/исполнение любому пользователю в системе)

или

chmod +rx scriptname (выдача прав на чтение/исполнение любому пользователю в системе)
chmod u+rx scriptname (выдача прав на чтение/исполнение только "владельцу" скрипта)


После того, как вы сделаете файл сценария исполняемым, вы можете запустить его примерно такой командой ./scriptname. Если, при этом, текст сценария начинается с корректной сигнатуры ("sha-bang"), то для его исполнения будет вызван соответствующий интерпретатор.
И наконец, завершив отладку сценария, вы можете поместить его в каталог /usr/local/bin(естественно, что для этого вы должны обладать правами root), чтобы сделать его доступным для себя и других пользователей системы. После этого сценарий можно вызвать, просто напечатав название файла в командной строке и нажав клавишу [ENTER].

Упражнения

1) Системные администраторы часто создают скрипты для автоматизации своего труда. Подумайте, для выполнения каких задач могут быть написаны сценарии.
2) Напишите сценарий, который выводит дату, время, список зарегистрировавшихся пользователей, и uptime системы и сохраняет эту информацию в системном журнале.

Часть вторая. Основы

Служебные символы

Служебные символы, используемые в текстах сценариев:

  • #Комментарии.


Строки, начинающиеся с символа # (за исключением комбинации #!) являются комментариями.

# Эта строка — комментарий.

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

echo "Далее следует комментарий." # Это комментарий.

Комментариям могут предшествовать пробелы (пробел, табуляция).

# Перед комментарием стоит символ табуляции.

Исполняемые команды не могут следовать за комментарием в той же самой строке. Пока что еще не существует способа отделения комментария от "исполняемого кода", следующего за комментарием в той же строке.
Само собой разумеется, экранированный символ # в операторе echo не воспринимается как начало комментария. Более того, он может использоваться в операциях подстановки параметров и в константных числовых выражениях.

echo "Символ # не означает начало комментария."
echo 'Символ # не означает начало комментария.'
echo Символ \# не означает начало комментария.
echo А здесь символ # означает начало комментария.
echo ${PATH#*:} # Подстановка — не комментарий.
echo $(( 2#101011 )) # База системы счисления — не комментарий.

Двойные кавычки("), одинарная кавычка(') и символ обратного "слеша"(\) экранируют действие символа #. В операциях поиска по шаблону символ # так же не воспринимается как начало комментария.

  • ;Разделитель команд. [Точка-с-запятой]

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

echo hello; echo there 
if [ -x "$filename" ]; then # Обратите внимание: "if" И "then" разделены точкой с запятой. 
# Почему? 
echo "Файл $filename найден."; cp $filename $filename.bak
  else echo "Файл $filename не найден."; touch $filename
fi; 
echo "Конец."

Следует отметить, что символ ; иногда так же как и # необходимо экранировать.

  • ;;Ограничитель в операторе выбора case. [Двойная-точка-с-запятой]
case "$variable" 
in 
abc) echo "$variable = abc" ;;
xyz) echo "$variable = xyz" ;;
esac
  • .Команда "точка"(.)

Эквивалент команды source. Это встроенная команда bash. "точка" может являться частью имени файла. Если имя файла начинается с точки, то это "скрытый" файл, т.е. команда ls при обычных условиях его не отображает:

Ls hidden.PNG


Если подразумевается имя каталога, то одна точка означает текущий каталог и две точки — каталог уровнем выше или родительский каталог:

Parent dir.PNG

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

bash$ cp /home/bozo/current_work/junk/* .

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

  • "Двойные кавычки(")

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

  • 'Одинарные кавычки(')

Одинарные кавычки 'STRING' экранируют все служебные символы в строке STRING. Это более строгая форма экранирования.

  • ,Запятая(,)

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

let "t2 = ((a = 9, 15 / 3))" # Присваивает значение переменной "a" и вычисляет "t2".
  • \Обратный слэш(\)

Комбинация \X "экранирует" символ X. Аналогичный эффект имеет комбинация с "одинарными кавычками", т.е. 'X'. Символ \ может использоваться для экранирования кавычек " и '.

  • /Слэш(/)

Разделитель, используемый в указании пути к каталогам и файлам. Слэш отделяет элементы пути к каталогам и файлам (например, /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

Как символ-заполнитель в операциях, которые предполагают наличие двух операндов:

: ${username=`whoami`}
# ${username=`whoami`} без символа : выдает сообщение об ошибке,
# если "username" не является командой...

Как символ-заполнитель для оператора вложенного документа. В операциях с подстановкой параметров:

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

В операциях замены подстроки с подстановкой значений переменных. В комбинации с оператором > (оператор перенаправления вывода), усекает длину файла до нуля. Если указан несуществующий файл — то он создаётся.

: > data.xxx # Файл "data.xxx" — пуст
# Тот же эффект имеет команда cat /dev/null >data.xxx
# Однако в данном случае не производится создание нового процесса, поскольку ":" является встроенной командой.

В комбинации с оператором >> — если задано имя существующего файла, то эта комбинация на него никак не влияет (: >> target_file). Если задано имя несуществующего файла, то он создаётся. Вышеизложенное применимо только к обычным файлам и неприменимо к конвейерам, символическим ссылкам и другим специальным файлам. Символ : может использоваться для создания комментариев, хотя и не рекомендуется. Если строка комментария начинается с символа #, то такая строка не проверяется интерпретатором на наличие ошибок. Однако в случае оператора : это не так.

: Это комментарий, который генерирует сообщение об ошибке, ( if [ $x -eq 3] ).

Символ : может использоваться как разделитель полей в /etc/passwd и переменной $PATH.

bash$ echo $PATH
/usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/sbin:/usr/sbin:/usr/games
  • !Восклицательный знак(!)

Инверсия (или логическое отрицание) используемое в условных операторах. Оператор ! инвертирует код завершения команды, к которой он применен. Также используется для логического отрицания в операциях сравнения, например, операция сравнения "равно"(=), при использовании оператора отрицания, преобразуется в операцию сравнения — "не равно" ( != ). Символ ! является зарезервированным ключевым словом BASH. В некоторых случаях символ ! используется для косвенного обращения к переменным. Кроме того, из командной строки оператор ! запускает механизм историй Bash. Примечательно, что этот механизм недоступен из сценариев(т.е. исключительно из командной строки).

  • *Символ "звёздочка"(*)

Символ * ("звёздочка") служит "шаблоном" для подстановки в имена файлов. Одиночный символ * означает любое имя файла в заданном каталоге.

bash$ echo *
abs-book.sgml add-drive.sh agram.sh alias.sh

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

  • ?Вопросительный знак(?)

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

  • $Символ доллара($)

Символ $ предшествующий имени переменной, указывает на то, что будет получено значение переменной:

var1=5
var2=23skidoo
echo $var1 # 5
echo $var2 # 23skidoo

В регулярных выражениях, символ $ обозначает конец строки. ${ } — подстановка параметра. $*,$@ — параметры командной строки. $? — Переменная "$?" хранит код завершения последней выполненной команды, функции или сценария. $$ — Переменная "$$" хранит id процесса сценария.

  • ( )Круглые скобки(())

Команды, заключённые в круглые скобки (( )) исполняются в дочернем процессе — subshell.

(a=hello; echo $a)

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

a=123
( a=321; )
echo "a = $a" # a = 123
#Переменная "a" в скобках подобна локальной переменной.

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

Array=(element1 element2 element3)

(( )) — двойные круглые скобки.Вычисляется целочисленное выражение, заключенное между двойными круглыми скобками (( )).

  • {}Фигурные скобки({})

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

grep Linux file*.{txt,htm*}# Поиск всех вхождений слова "Linux"
# в файлах "fileA.txt", "file2.txt", "fileR.html", "file-87.htm", и пр.

Использование неэкранированных или неокавыченных пробелов внутри фигурных скобок недопустимо!

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 (значение, присвоенное во вложенном блоке кода)

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

Пример.: Вложенные блоки и перенаправление ввода-вывода.

#!/bin/bash
# Чтение строк из файла /etc/fstab.
File=/etc/fstab
{read line1
read line2
} < $File
echo "Первая строка в $File :"
echo "$line1"
echo
echo "Вторая строка в $File :"
echo "$line2"
exit 0

Пример.: Сохранение результата исполнения вложенного блока в файл

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

В отличие от групп команд в (круглых скобках), описаных выше, вложенные блоки кода, заключённые в {фигурные скобки} исполняются в пределах того же процесса, что и сам скрипт (т.е. не вызывают запуск дочернего процесса — subshell)!
{ } \;pathname, полное имя файла (т.е. путь к файлу и его имя). Чаще всего используется совместно с командой find. Обратите внимание на то, что символ ; , которым завершается ключ -exec команды find, экранируется обратным слэшем. Это необходимо, чтобы предотвратить его интерпретацию.

  • [ ]Квадратные скобки([ ])

Проверка истинности выражения, заключенного в квадратные скобки [ ]. Примечательно, что [ является частью встроенной команды test (и её синонимом), и не имеет никакого отношения к "внешней" утилите /usr/bin/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) — Подстановка процесса. В операциях сравнения, символы "<" и ">" обозначают операции сравнения строк. А так же операции сравнения целых чисел. <, > — Посимвольное ASCII-сравнение.

veg1=carrots
veg2=tomatoes
if [[ "$veg1" < "$veg2" ]]; then
 echo "Не смотря на то, что в словаре слово $veg1 предшествует слову $veg2,"
 echo "это никак не отражает мои кулинарные предпочтения."
else
 echo "Интересно. Каким словарем вы пользуетесь?"
fi

\<,\> — границы отдельных слов в регулярных выражениях.

bash$ grep '\<the\>' textfile
  • | — Конвейер(pipe).

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

echo ls -l | sh
# Передаёт вывод "echo ls -l" командному интерпретатору shell,
# тот же результат даёт простая команда "ls -l".
cat *.lst | sort | uniq
# Объединяет все файлы ".lst", сортирует содержимое и удаляет повторяющиеся строки.
Конвейеры (еще их называют каналами) — это классический способ взаимодействия процессов, с помощью которого stdout одного процесса перенаправляется на stdin другого. Обычно используется совместно с командами вывода, такими как cat или echo, от которых поток данных поступает в "фильтр" (команда, которая на входе получает данные, преобразует их и обрабатывает).
cat $filename | grep $search_word

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

#!/bin/bash
# uppercase.sh : Преобразование вводимых символов в верхний регистр.
tr 'a-z' 'A-Z'
# Диапазоны символов должны быть заключены в кавычки
# чтобы предотвратить порождение имен файлов от однобуквенных имен файлов.
exit 0

А теперь попробуем объединить в конвейер команду ls -l с этим сценарием:

Uppercase 0.PNG

Выход 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

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

Пример.: Запуск цикла в фоновом режиме

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

Команда, исполняемая в пределах сценария в фоне, может подвесить сценарий, ожидая нажатия клавиши. К счастью, это легко "лечится".

  • && логическая операция 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.

Пример.: Резервное архивирование всех файлов, которые были изменены втечение последних суток

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


Могут возникнуть конфликтные ситуации между опреатором перенаправления - и именами файлов, начинающимися с символа "-". Поэтому сценарий должен проверять имена файлов и предварять их префиксом пути, например, ./-FILENAME, $PWD/-FILENAME или $PATHNAME/-FILENAME. Если значение переменной начинается с символа "-", то это тоже может быть причиной появления ошибок.

var="-n"
echo $var
# В данном случае команда приобретет вид "echo -n" и ничего не выведет.

- — предыдущий рабочий каталог. Команда cd - выполнит переход в предыдущий рабочий каталог, путь к которому хранится в переменной окружения $OLDPWD. Не путайте оператор "-" (предыдущего рабочего каталога) с оператором - (переназначения). Ещё раз напомню, что интерпретация символа "-" зависит от контекста, в котором он употребляется. - — Знак минус в арифметических операциях.

  • = Символ равно (=).

Символ равно (=) выступает в качестве оператора присваивания.

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 + <клавиша>.

  1. Ctrl-B — Курсор — на одну позицию назад (без стирания символа).
  2. Ctrl-C — Прерывание выполнения процесса.
  3. Ctrl-D — Выход из командного интерпретатора (log out) (аналог команды exit)."EOF" (признак конца файла). Этот символ может выступать в качестве завершающего привводе с stdin.
  4. Ctrl-G — "BEL" (звуковой сигнал -- "звонок").
  5. Ctrl-H — Backspace ("забой") — удаление предыдущего символа.
  6. Ctrl-I — Горизонтальная табуляция.
  7. Ctrl-J — Перевод строки.
  8. Ctrl-K — Вертикальная табуляция.
  9. Ctrl-L — Перевод формата (очистка экрана (окна) терминала). Аналогична команде clear.
  10. Ctrl-M — Возврат каретки.
  11. Ctrl-Q — Resume (XON). Эта комбинация "восстанавливает" работу stdin терминала.
  12. Ctrl-S — Suspend (XOFF). Эта комбинация "замораживает" stdin терминала. (Для восстановления "работоспособности" — используйте комбинацию Ctrl-Q)
  13. Ctrl-U — Стирание строки ввода.
  14. Ctrl-Z — Приостановка процесса.
#!/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
 #!/bin/bash
 # Спасибо Lee Maschmeyer, за этот пример.
 read -n 1 -s -p $'Control-M — переводит курсор в начало этой же строки. Нажмите клавишу Enter. \x0d'
 # Разумеется, что, 'x0d' — это # шестнадцатиричный эквивалент  '''''Ctrl-M'''''.
 echo >&2 # Перевод строки 
 read -n 1 -s -p $'Control-J — переводит курсор в начало другой строки. \x0a'
 echo >&2 # Control-J — это перевод строки.
 read -n 1 -s -p $'А Control-K\x0b — вниз.'
 echo >&2 # Control-K — это вертикальная табуляция.
 exit 0
  • Символ пробела ( ).

Пробельный символ используется как разделитель команд или переменных. В качестве пробельного символа могут выступать — собственно пробел (space), символ табуляции, символ перевода строки, символ возврата каретки или комбинация из вышеперечисленных символов. В некоторых случаях, таких как присваивание значений переменным, использование пробельных символов недопустимо. Пустые строки никак не обрабатываются командным интерпретатором и могут свободно использоваться для визуального выделения отдельных блоков сценария. $IFS — переменная специального назначения. Содержит символы-разделители полей, используемые некоторыми командами. По-умолчанию — пробельные символы.

Переменные и параметры. Введение.

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

Подстановка переменных

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

  • $


Необходимо всегда помнить о различиях между именем переменной и её значением. Если variable1 — это имя переменной, то $variable1 — это ссылка на её значение. "Чистые" имена переменных, без префикса $, могут использоваться только при объявлении переменной, при присваивании переменной некоторого значения, при удалении (сбросе), при экспорте и в особых случаях — когда переменная представляет собой название сигнала. Присваивание может производится с помощью символа = (например: var1=27), инструкцией read и в заголовке цикла (for var2 in 1 2 3).
Заключение ссылки на переменную в двойные кавычки (" ") никак не сказывается на работе механизма подстановки. Этот случай называется "частичные кавычки", иногда можно встретить название "нестрогие кавычки". Одиночные кавычки (' ') заставляют интерпретатор воспринимать ссылку на переменную как простой набор символов, потому в одинарных кавычках операции подстановки не производятся. Этот случай называется "полные", или "строгие" кавычки.
Примечательно, что написание $variable фактически является упрощенной формой написания ${variable}. Более строгая форма записи ${variable} может с успехом использоваться в тех случаях, когда применение упрощенной формы записи порождает сообщения о синтаксических ошибках.

Пример: Присваивание значений переменным и подстановка значений переменных.

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

Неинициализированная переменная хранит "пустое" значение - не ноль! Использование неинициализированных переменных может приводить к ошибкам разного рода в процессе исполнения. Не смотря на это в арифметических операциях допускается использовать неинициализированные переменные.

echo "$uninitialized" # (пустая строка)
let "uninitialized += 5" # Прибавить 5.
echo "$uninitialized" # 5
# Заключение:
# Неинициализированные переменные не имеют значения, однако
# в арифметических операциях за значение таких переменных принимается число 0.
# Это недокументированная (и возможно непереносимая) возможность.

Присваивание значений переменным

  • =


Оператор присваивания (пробельные символы до и после оператора — недопустимы). Не путайте с операторами сравнения = и -eq! Обратите внимание: символ = может использоваться как в качестве оператора присваивания, так и в качестве оператора сравнения, конкретная интерпретация зависит от контекста применения.

Пример: Простое присваивание.

#!/bin/bash
# Явные переменные
echo
# Когда перед именем переменной не употребляется символ '$'?
# В операциях присваивания.
# Присваивание
a=879
echo "Значение переменной \"a\"$a."
# Присваивание с помощью ключевого слова 'let'
let a=16+5
echo "Значение переменной \"a\" теперь стало равным: $a."
echo
# В заголовке цикла 'for' (своего рода неявное присваивание)
echo -n "Значения переменной \"a\" в цикле: "
for a in 7 8 9 11
do
 echo -n "$a "
done
echo
echo
# При использовании инструкции 'read' (тоже одна из разновидностей присваивания)
echo -n "Введите значение переменной \"a\" "
read a
echo "Значение переменной \"a\" теперь стало равным: $a."
echo
exit 0


Пример: Присваивание значений переменным простое и замаскированное.

#!/bin/bash
a=23 # Простейший случай
echo $a
b=$a
echo $b 
# Теперь немного более сложный вариант (подстановка команд).
a=`echo Hello!` # В переменную 'a' попадает результат работы команды 'echo'
echo $a
# Обратите внимание на восклицательный знак (!) в подстанавливаемой команде
# этот вариант не будет работать при наборе в командной строке,
# поскольку здесь используется механизм "истории команд" BASH
# Однако, в сценариях, механизм истории команд запрещен.
a=`ls -l` # В переменную 'a' записывается результат работы команды 'ls -l'
echo $a # Кавычки отсутствуют, удаляются лишние пробелы и пустые строки.
echo
echo "$a" # Переменная в кавычках, все пробелы и пустые строки сохраняются
exit 0


# Взято из /etc/rc.d/rc.local
R=$(cat /etc/redhat-release)
arch=$(uname -m)

Переменные Bash не имеют типа

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

Пример: Целое число или строка?

#!/bin/bash
# int-or-string.sh: Целое число или строка?
a=2334 # Целое число.
let "a += 1"
echo "a = $a " # a = 2335
echo # Всё ещё целое число.
b=${a/23/BB} # замена "23" на "BB".
# Происходит трансформация числа в строку.
echo "b = $b" 
# b = BB35
declare -i b # Явное указание типа здесь не поможет.
echo "b = $b" # b = BB35
let "b += 1" # BB35 + 1 =
echo "b = $b" # b = 1
echo
c=BB34
echo "c = $c" # c = BB34
d=${c/BB/23} # замена "BB" на "23". 
# Переменная $d становится целочисленной.
echo "d = $d" # d = 2334
let "d += 1" # 2334 + 1 =
echo "d = $d" # d = 2335echo
echo
# А что происходит с "пустыми" переменными?
e=""
echo "e = $e" # e =
let "e += 1" # Арифметические операции допускают использование "пустых" переменных?
echo "e = $e" # e = 1
echo # "Пустая" переменная становится целочисленной.
# А что происходит с необъявленными переменными?
echo "f = $f" # f =
let "f += 1" # Арифметические операции допустимы?
echo "f = $f" # f = 1
echo # Необъявленная переменная трансформируется в целочисленную.
# Переменные Bash не имеют типов.
exit 0


Отсутствие типов — это и благословение и проклятие. С одной стороны — отсутствие типов делает сценарии более гибкими(чтобы повеситься — достаточно иметь веревку!) и облегчает чтение кода. С другой — является источником потенциальных ошибок и поощряет привычку к "неряшливому" программированию. Бремя отслеживания типа той или иной переменной полностью лежит на плечах программиста. Bash не будет делать это за вас!

Специальные типы переменных

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

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

  • переменные окружения

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

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

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

Пространство, выделяемое под переменные окружения, ограничено. Создание слишком большого количества переменных 
окружения или одной переменной, которая занимает слишком большое пространство, может привести к возникновению определённых проблем.
bash$ eval "`seq 10000 | sed -e 's/.*/export var&=ZZZZZZZZZZZZZZ/'`"
bash$ du
bash: /usr/bin/du: Argument list too long

Если сценарий изменяет переменные окружения, то они должны "экспортироваться", т.е. передаваться окружению, локальному по отношению к сценарию. Эта функция возложена на команду export.

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

Аргументы, передаваемые скрипту из командной строки — $0, $1, $2, $3..., где $0 — это название файла сценария, $1 — это первый аргумент, $2 — второй, $3 — третий и так далее. Аргументы, следующие за $9, должны заключаться в фигурные скобки, например: ${10}, ${11}, ${12}.
Специальные переменные $* и $@ содержат все позиционные параметры (аргументы командной строки).

Пример: Позиционные параметры.

#!/bin/bash
# Команда вызова сценария должна содержать по меньшей мере 10 параметров, например
# ./scriptname 1 2 3 4 5 6 7 8 9 10
MINPARAMS=10
echo
echo "Имя файла сценария: \"$0\"."
# Для текущего каталога добавит ./
echo "Имя файла сценария: \"`basename $0`\"."
# Добавит путь к имени файла (см. 'basename')
echo
if [ -n "$1" ] # Проверяемая переменная заключена в кавычки.
then
 echo "Параметр #1: $1" # необходимы кавычки для экранирования символа #
fi
if [ -n "$2" ]
then
 echo "Параметр #2: $2"
fi
if [ -n "$3" ]
then
 echo "Параметр #3: $3"
fi
if [ -n "${10}" ] # Параметры, следующие за $9 должны заключаться в фигурные скобки
then 
 echo "Параметр #10: ${10}"
fi
echo "-----------------------------------"
echo "Все аргументы командной строки: "$*""
if [ $# -lt "$MINPARAMS" ]
then
 echo
echo "Количество аргументов командной строки должно быть не менее $MINPARAMS !"
fi
echo
exit 0

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

args=$# # Количество переданных аргументов.
lastarg=${!args} # Обратите внимание: lastarg=${!$#} неприменимо.

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

Если сценарий ожидает передачи аргументов в командной строке, то при их отсутствии он получит "пустые" переменные, что может вызвать нежелательный
побочный эффект. Один из способов борьбы с подобными ошибками — добавить дополнительный символ в обеих частях операции присваивания, 
где используются аргументы командной строки.
variable1_=$1_# Это предотвратит появление ошибок, даже при отсутствии входного аргумента.
critical_argument01=$variable1_
# Дополнительные символы всегда можно "убрать" позднее.
# Это может быть сделано примерно так:
variable1=${variable1_/_/} # Побочный эффект возникает только если имя переменной # $variable1_ будет начинаться с символа "_".
# Здесь используется один из вариантов подстановки параметров, обсуждаемых далее.
# Отсутствие шаблона замены приводит к удалению.
# Более простой способ заключается
# в обычной проверке наличия позиционного параметра.
if [ -z $1 ]; then 
exit $POS_PARAMS_MISSING
fi

Пример: wh, whois выяснение имени домена.

#!/bin/bash
# Команда 'whois domain-name' выясняет имя домена на одном из 3 серверов:
# ripe.net, cw.net, radb.net
# Разместите этот скрипт под именем 'wh' в каталоге /usr/local/bin
# Требуемые символические ссылки:
# ln -s /usr/local/bin/wh /usr/local/bin/wh-ripe
# ln -s /usr/local/bin/wh /usr/local/bin/wh-cw
# ln -s /usr/local/bin/wh /usr/local/bin/wh-radb
if [ -z "$1" ]
then
 echo "Порядок использования: `basename $0` [domain-name]"
exit 65
fi
case `basename $0` in
# Проверка имени скрипта и, соответственно, имени сервера
"wh" ) whois $1@whois.ripe.net;;
"wh-ripe") whois $1@whois.ripe.net;;
"wh-radb") whois $1@whois.radb.net;;
"wh-cw" ) whois $1@whois.cw.net;;
* ) echo "Порядок использования: `basename $0` [domain-name]";;
esac
exit 0

Команда shift "сдвигает" позиционные параметры, в результате чего парметры "сдвигаются" на одну позицию влево.
$1 <--- $2, $2 <--- $3, $3 <--- $4, и т.д.
Прежний аргумент $1 теряется, но аргумент $0 (имя файла сценария) остаётся без изменений. Если вашему сценарию передаётся большое количество входных аргументов, то команда shift позволит вам получить доступ к аргументам, с порядковым номером больше 9, без использования {фигурных скобок}.

Пример: Использование команды shift.

#!/bin/bash
# Использование команды 'shift' с целью перебора всех аргументов командной строки.
# Назовите файл с этим сценарием, например "shft",
# и вызовите его с набором аргументов, например:
# ./shft a b c def 23 skidoo
until [ -z "$1" ] # До тех пор пока не будут разобраны все входные аргументы...
do
 echo -n "$1 " 
 shift
done
echo # Дополнительная пустая строка.
exit 0

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

Кавычки

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

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

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

bash$ grep '[Пп]ервая' *.txt
file1.txt:Это первая строка в file1.txt. 
file2.txt:Это Первая строка в file2.txt.

Примечательно, что "не окавыченный" вариант команды grep [Пп]ервая *.txt будет правильно исполняться в Bash, но не в tcsh. Вообще, желательно использовать двойные кавычки (" ") при обращении к переменным. Это предотвратит интерпретацию специальных символов, которые могут содержаться в именах переменных, за исключением $, `(обратная кавычка) и \ (escape — обратный слэш). То, что символ $ попал в разряд исключений, позволяет выполнять обращение к переменным внутри строк, ограниченных двойными кавычками ("$variable"), т.е. выполнять подстановку значений переменных. Двойные кавычки могут быть использованы для предотвращения разбиения строки на слова. Заключение строки в кавычки приводит к тому, что она передаётся как один аргумент, даже если она содержит пробельные символы-разделители.

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 пробелами).

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

Пример: Вывод "причудливых" переменных.

#!/bin/bash
# weirdvars.sh: Вывод "причудливых" переменных
var="'(]\\{}\$\""
echo $var # '(]\{}$"
echo "$var" # '(]\{}$" Никаких различий.
echo
IFS='\'
echo $var # '(] {}$" \ символ-разделитель преобразован в пробел.
echo "$var" # '(]\{}$"
# Примеры выше предоставлены S.C.
exit 0

Одиночные кавычки (' ') схожи по своему действию с двойными кавычками, только не допускают обращение к переменным, поскольку специальный символ "$" внутри одинарных кавычек воспринимается как обычный символ. Внутри одиночных кавычек, любой специальный символ, заисключением ', интерпретируется как простой символ. Одиночные кавычки ("строгие, или полные кавычки") следует рассматривать как более строгий вариант, чем двойные кавычки ("нестрогие, или неполные кавычки"). Поскольку внутри одиночных кавычек даже экранирующий (\) символ воспринимается как обычный символ, попытка вывести одиночную кавычку внутри строки, ограниченной одинарными кавычками, не даст желаемого результата.

echo "Why can't I write 's between single quotes"
echo
# Обходной метод.
echo 'Why can'\''t I write '"'"'s between single quotes'
# |-------| |----------| |-----------------------|
# Три строки, ограниченных одинарными кавычками,
# и экранированные одиночные кавычки между ними.
# Пример любезно предоставлен Stephane Chazelas.

Экранирование — это способ заключения в кавычки одиночного символа. Экранирующий (escape)символ (\) сообщает интерпретатору, что следующий за ним символ должен восприниматься как обычный символ. С отдельными командами и утилитами, такими как echo и sed, экранирующий символ может применяться для получения обратного эффекта — когда обычные символы при экранировании приобретают специальное значение.

Специальное назначение некоторых экранированных символов, используемых совместно с echo и sed

  • \n — перевод строки (новая строка)
  • \r — перевод каретки
  • \t — табуляция
  • \v — вертикальная табуляция
  • \b — забой (backspace)
  • \a — "звонок" (сигнал)
  • \0xx — ASCII-символ с кодом 0xx в восьмеричном виде

Пример: Экранированные символы.

#!/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
  • \" — кавычки
echo "Привет" # Привет
echo "Он сказал: \"Привет\"." # Он сказал: "Привет".
  • \$ — символ доллара (если за комбинацией символов \$ следует имя переменной, то она не будет разыменована)
echo "\$variable01" # выведет $variable01
  • \\ — обратный слэш
echo "\\" # выведет \
# Тогда как...
echo "\" # Приведёт к выводу вторичного приглашения к вводу. 
# В сценариях — порождает сообщение об ошибке.

Поведение символа \ сильно зависит от того экранирован ли он, ограничен ли кавычками или находится внутри конструкции подстановки команды или во вложенном документе.

# Простое экранирование и кавычки
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=\\  # \
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_listecho "-------------------------------------------------------------------------"
# Что произойдет, если экранировать пробелы в списке?
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.)

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

echo "foo
bar"
#foo
#bar
echo
echo 'foo
bar' # Никаких различий.
echo
echo foo\
bar # Перевод строки экранирован.
#foobar
echo
echo "foo\
bar" # Внутри "нестрогих" кавычек символ "\" интерпретируется как экранирующий.
#foobar
echo
echo 'foo\
bar' # В "строгих" кавычках обратный слэш воспринимается как обычный символ.
#foo\
#bar
# Примеры предложены Stephane Chazelas.

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

...эта часть Bourne shell покрыта мраком, тем не менее все пользуются ею. ©Chet Ramey

Команда exit может использоваться для завершения работы сценария, точно так же как и в программах на языке C. Кроме того, она может возвращать некоторое значение, которое может быть проанализировано вызывающим процессом. Каждая команда возвращает код завершения (иногда код завершения называют возвращаемым значением). В случае успеха команда должна возвращать 0, а в случае ошибки — ненулевое значение, которое, как правило, интерпретируется как код ошибки. Практически все команды и утилиты Unix возвращают 0 в случае успешного завершения, но имеются и исключения из правил.
Аналогичным образом ведут себя функции, расположенные внутри сценария, и сам сценарий, возвращая код завершения. Код, возвращаемый функцией или сценарием, определяется кодом возврата последней команды. Команде exit можно явно указать код возврата, в виде: exit nnn, где nnn — это код возврата (число в диапазоне 0 - 255). Когда работа сценария завершается командой exit без параметров, то код возврата сценария определяется кодом завершения последней исполненной команды (не считая саму команду exit).

#!/bin/bash
COMMAND_1
...
# Сценарий вернет код завершения последней команды.
COMMAND_LAST
exit

Эквивалентный вариант — exit $? или можно вообще опустить команду exit.

#!/bin/bash
COMMAND_1
...
# Сценарий вернет код завершения последней команды.
COMMAND_LAST
exit $?
 
#!/bin/bash
COMMAND1
...
# Сценарий вернет код завершения последней команды.
COMMAND_LAST

Код возврата последней команды хранится в специальной переменной $?. После исполнения кодафункции, переменная $? хранит код завершения последней команды, исполненной в функции. Таким способом в Bash передаётся "значение, возвращаемое" функцией. После завершения работы сценария, код возврата можно получить, обратившись из командной строки к переменной $?, т.е. это будет код возврата последней команды, исполненной в сценарии.

Пример: Завершение / код завершения.

#!/bin/bash
echo hello
echo $? # код возврата = 0, поскольку команда выполнилась успешно.
lskdf # Несуществующая команда.
echo $? # Ненулевой код возврата, поскольку команду выполнить не удалось.
echo
exit 113 # Явное указание кода возврата 113.
# Проверить можно, если набрать в командной строке "echo $?"
# после выполнения этого примера.
# В соответствии с соглашениями, 'exit 0' указывает на успешное завершение,
# в то время как ненулевое значение означает ошибку.

Переменная $? особенно полезна, когда необходимо проверить результат исполнения команды. Символ !, может выступать как логическое "НЕ" для инверсии кода возврата.

Пример: Использование символа "!" для логической инверсии кода возврата.

#!/bin/bash
true # встроенная команда "true".
echo "код возврата команды \"true\" = $?" # 0
! true
echo "код возврата команды \"! true\" = $?" # 1
# Обратите внимание: символ "!" от команды необходимо отделять пробелом.
# !true вызовет сообщение об ошибке "command not found"
# Спасибо S.C.

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

Проверка условий

Практически любой язык программирования включает в себя условные операторы, предназначенные для проверки условий, чтобы выбрать тот или иной путь развития событий в зависимости от этих условий. В Bash, для проверки условий, имеется команда test, различного вида скобочные операторы и условный оператор if/then.

Конструкции проверки условий

  • Оператор if/then проверяет — является ли код завершения списка команд 0 (поскольку 0 означает "успех"!), и если это так, то выполняет одну, или более, команд, следующие за словом then.
  • Существует специальная команда — [ (левая квадратная скобка). Она является синонимом команды test, и является встроенной командой (т.е. более эффективной, в смысле производительности). Эта команда воспринимает свои аргументы как выражение сравнения или как файловую проверку и возвращает код завершения в соответствии с результатами проверки(0 — истина, 1 — ложь).
  • Начиная с версии 2.02, Bash предоставляет в распоряжение программиста конструкцию ... , расширенный вариант команды test, которая выполняет сравнение способом более знакомым программистам, пишущим на других языках программирования. Обратите внимание: [[ — это зарезервированное слово, а не команда!

Bash исполняет [[ $a -lt $b ]] как один элемент, который имеет код возврата!
Круглые скобки (( ... )) и предложение let ... так же возвращают код 0, если результатом арифметического выражения является ненулевое значение. Таким образом, арифметические выражения могут участвовать в операциях сравнения.
Предложение let "1<2" возвращает 0 (так как результат сравнения "1<2" — "1", или "истина")(( 0 && 1 )) возвращает 1 (так как результат операции "0 && 1" — "0", или "ложь")

  • Условный оператор if проверяет код завершения любой команды, а не только результат выражения, заключенного в квадратные скобки.
if cmp a b &> /dev/null # Подавление вывода.
then echo "Файлы a и b идентичны."
else echo "Файлы a и b имеют различия."
fi
if grep -q Bash file
then echo "Файл содержит, как минимум, одно слово Bash."
fi
word=Linux
letter_sequence=inu
if echo "$word" | grep -q "$letter_sequence"
# Подавление вывода ключом "-q" в команде grep.
then
 echo "Последовательность $letter_sequence обнаружена в слове $word"
else
 echo "Последовательность $letter_sequence, в слове $word не найдена"
fi
 
if COMMAND_WHOSE_EXIT_STATUS_IS_0_UNLESS_ERROR_OCCURRED
then echo "Команда выполнена успешно."
else echo "Обнаружена ошибка при выполнении команды."
fi
  • Оператор if/then допускает наличие вложенных проверок.
if echo "Следующий *if* находится внутри первого *if*."
 if [[ $comparison = "integer" ]]
   then (( a < b ))
 else
   [[ $a < $b ]]
 fi
then
 echo '$a меньше $b'fi
fi

Это детальное описание конструкции "if-test" любезно предоставлено Stephane Chazelas.

Пример: Что есть "истина"?

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

Упражнение

Объясните результаты, полученные в примере Что есть "истина"?

if [ condition-true ]
then 
 command 1
 command 2 
 ...
else # Необязательная ветка (можно опустить, если в ней нет необходимости). 
# Дополнительный блок кода, 
# исполняемый в случае, когда результат проверки — "ложь". 
command 3 command 4 ...
fi

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

if [ -x "$filename" ]; then
  • Else if и 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 требуют её наличие. Команда 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

Пример: Эквиваленты команды 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. Внутри этой конструкции не производится никакой дополнительной интерпретации имён файлов и не производится разбиение аргументов на отдельные слова, но допускается подстановка параметров и команд.

file=/etc/passwd
if [[ -e $file ]]
then
 echo "Файл паролей найден."
fi

Конструкция [[...]] более предпочтительна, нежели [...], поскольку поможет избежать некоторых логических ошибок. Например, операторы &&, ||, < и > внутри [[ ]] вполне допустимы, в то время как внутри [ ] порождают сообщения об ошибках! Строго говоря, после оператора 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 и [ ], обсуждавшимся выше.

Пример: Арифметические выражения внутри (( ))

#!/bin/bash
# Проверка арифметических выражений.
# Инструкция (( ... )) вычисляет арифметические выражения.
# Код возврата противоположен коду возврата инструкции [ ... ] !
(( 0 ))
echo "Код возврата \"(( 0 ))\": $?." # 1(( 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

Операции проверки файлов

Возвращает true если...

  • -e — файл существует
  • -fобычный файл (не каталог и не файл устройства!)
  • -s — ненулевой размер файла
  • -d — файл является каталогом
  • -b — файл является блочным устройством (floppy, cdrom и т.п.)
  • -c — файл является символьным устройством (клавиатура, модем, звуковая карта и т.п.)
  • -p — файл является каналом
  • -h — файл является символической ссылкой
  • -L — файл является символической ссылкой
  • -S — файл является сокетом
  • -t — файл (дескриптор) связан с терминальным устройством

Этот ключ может использоваться для проверки — является ли файл стандартным устройством ввода stdin ([ -t 0 ]) или стандартным устройством вывода stdout ([ -t 1 ]).

  • -r — файл доступен для чтения (пользователю, запустившему сценарий)
  • -w — файл доступен для записи (пользователю, запустившему сценарий)
  • -x — файл доступен для исполнения (пользователю, запустившему сценарий)
  • -g — set-group-id (sgid) флаг для файла или каталога установлен

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

  • -u — set-user-id (suid) флаг для файла установлен

Установленный флаг suid приводит к изменению привилегий запущенного процесса на привилегии владельца исполняемого файла. Исполняемые файлы, владельцем которых является root, с установленным флагом set-user-id запускаются с привилегиями root, даже если их запускает обычный пользователь. Это может оказаться полезным для некоторых программ(таких как pppd и cdrecord), которые осуществляют доступ к аппаратной части компьютера. В случае отсутствия флага suid, программы не смогут быть запущены рядовым пользователем, не обладающим привилегиями root.

-rwsr-xr-t 1 root 178236 Oct 2 2000 /usr/sbin/pppd

Файл с установленным флагом suid отображается с включенным флагом s в поле прав доступа.

  • -k — флаг sticky bit (бит фиксации) установлен

Общеизвестно, что флаг "sticky bit" — это специальный тип прав доступа к файлам. Программы сустановленным флагом "sticky bit" остаются в системном кэше после своего завершения,обеспечивая тем самым более быстрый запуск программы. Если флаг установлен для каталога, то это приводит к ограничению прав на запись. Установленный флаг "sticky bit" отображается в виде символа t в поле прав доступа.

drwxrwxrwt 7 root 1024 May 19 21:26 tmp/

Если пользователь не является владельцем каталога , с установленным "sticky bit", но имеет право на запись в каталог, то он может удалять только те файлы в каталоге, владельцем которых он является. Это предотвращает удаление и перезапись "чужих" файлов в общедоступных каталогах, таких как /tmp.

  • -O — вы являетесь владельцем файла
  • -G — вы принадлежите к той же группе, что и файл
  • -N — файл был модифицирован с момента последнего чтения
  • f1 -nt f2 — файл f1 более новый, чем f2
  • f1 -ot f2 — файл f1 более старый, чем f2
  • f1 -ef f2 — файлы f1 и f2 являются "жесткими" ссылками на один и тот же файл
  • ! — "НЕ" — логическое отрицание (инверсия) результатов всех вышеприведенных проверок(возвращается true если условие отсутствует).

Пример: Проверка "битых" ссылок.

#!/bin/bash
# broken-link.sh
# Автор Lee Bigelow <ligelowbee@yahoo.com>
# Используется с его разрешения.
#Сценарий поиска "битых" ссылок и их вывод в "окавыченном" виде
#таким образом они могут передаваться утилите xargs для дальнейшей обработки :)
#например. broken-link.sh /somedir /someotherdir|xargs rm
#
#На всякий случай приведу лучший метод:
#
#find "somedir" -type l -print0|\
#xargs -r0 file|\
#grep "broken symbolic"|
#sed -e 's/^\|: *broken symbolic.*$/"/g'
#
#но это не чисто BASH-евский метод, а теперь сам сценарий.
#Внимание! будьте осторожны с файловой системой /proc и циклическими ссылками!
##############################################################
#Если скрипт не получает входных аргументов,
#то каталогом поиска является текущая директория
#В противном случае, каталог поиска задается из командной строки
####################
[ $# -eq 0 ] && directorys=`pwd` || directorys=$@
#Функция linkchk проверяет каталог поиска
#на наличие в нём ссылок на несуществующие файлы, и выводит их имена.
#Если анализируемый файл является каталогом,
#то он передаётся функции linkcheck рекурсивно.
##########
linkchk () {
 for element in $1/*; do
 [ -h "$element" -a ! -e "$element" ] && echo \"$element\"
 [ -d "$element" ] && linkchk $element
 # Само собой, '-h' проверяет символические ссылки, '-d' -- каталоги.
 done
}
#Вызов функции linkchk для каждого аргумента командной строки,
#если он является каталогом. Иначе выводится сообщение об ошибке
#и информация о порядке пользования скриптом.
################
for directory in $directorys; do
 if [ -d $directory ]
  then linkchk $directory
  else
    echo "$directory не является каталогом"
    echo "Порядок использования: $0 dir1 dir2 ..."
 fi
done
exit 0

Операции сравнения

Сравнение целых чисел:

  • -eq — равно
if [ "$a" -eq "$b" ]
  • -ne — не равно
if [ "$a" -ne "$b" ]
  • -gt — больше
if [ "$a" -gt "$b" ]
  • -ge — больше или равно
if [ "$a" -ge "$b" ]
  • -lt — меньше
if [ "$a" -lt "$b" ]
  • -le — меньше или равно
if [ "$a" -le "$b" ]
  • < — меньше (внутри двойных круглых скобок )
(("$a" < "$b"))
  • <= — меньше или равно (внутри двойных круглых скобок)
(("$a" <= "$b"))
  • > — больше (внутри двойных круглых скобок )
(("$a" > "$b"))
  • >= — больше или равно(внутри двойных круглых скобок )
(("$a" >= "$b"))

Cравнение строк:

  • = — равно
if [ "$a" = "$b" ]
  • == — равно. Синоним оператора =.
if [ "$a" == "$b" ]
[[ $a == z* ]] # истина, если $a начинается с символа "z" (сравнение по шаблону)
[[ $a == "z*" ]] # истина, если $a равна z*
[ $a == z* ] # имеют место подстановка имён файлов и разбиение на слова
[ "$a" == "z*" ] # истина, если $a равна z*
# Спасибо S.C.
  • != — не равно
if [ "$a" != "$b" ]
Этот оператор используется при поиске по шаблону внутри  ... 
  • < — меньше, в смысле величины ASCII-кодов
if [[ "$a" < "$b" ]]
if [ "$a" \< "$b" ]
Обратите внимание! Символ "<" необходимо экранировать внутри [ ]
  • > — больше, в смысле величины ASCII-кодов
if [[ "$a" > "$b" ]]
if [ "$a" \> "$b" ]
Обратите внимание! Символ ">" необходимо экранировать внутри [ ]
  • -z — строка "пустая", т.е. имеет нулевую длину
  • -n — строка не "пустая"

Оператор -n требует, чтобы строка была заключена в кавычки внутри квадратных скобок. Как правило, проверка строк, не заключенных в кавычки, оператором ! -z, или просто указание строки без кавычек внутри квадратных скобок, проходит нормально, однако это небезопасная, с точки зрения отказоустойчивости, практика. Всегда заключайте проверяемую строку в кавычки.

Пример: Операции сравнения.

#!/bin/bash
a=4
b=5
# Здесь переменные "a" и "b" могут быть как целыми числами, так и строками.
# Здесь наблюдается некоторое размывание границ
# между целочисленными и строковыми переменными,
# поскольку переменные в Bash не имеют типов.
# Bash выполняет целочисленные операции над теми переменными,
# которые содержат только цифры
# Будьте внимательны!
echo
if [ "$a" -ne "$b" ]
then
 echo "$a не равно $b"
 echo "(целочисленное сравнение)"
fi
echo
if [ "$a" != "$b" ]
then
 echo "$a не равно $b."
 echo "(сравнение строк)"
 # "4" != "5"
 # ASCII 52 != ASCII 53
fi
# Оба варианта, "-ne" и "!=", работают правильно.
echo
exit 0

Пример: Проверка — является ли строка пустой

#!/bin/bash
# str-test.sh: Проверка пустых строк и строк, не заключенных в кавычки,
# Используется конструкция if [ ... ]
# Если строка не инициализирована, то она не имеет никакого определенного значения.
# Такое состояние называется "null" (пустая) (это не то же самое, что ноль).
if [ -n $string1 ] # $string1 не была объявлена или инициализирована.
then
 echo "Строка \"string1\" не пустая."
else
 echo "Строка \"string1\" пустая."
fi
# Неверный результат.
# Выводится сообщение о том, что $string1 не пустая,
# не смотря на то, что она не была инициализирована.
echo
# Попробуем ещё раз.
if [ -n "$string1" ] # На этот раз, переменная $string1 заключена в кавычки.
then
 echo "Строка \"string1\" не пустая."
else
 echo "Строка \"string1\" пустая."
fi # Внутри квадратных скобок заключайте строки в кавычки!
echo
if [ $string1 ] # Опустим оператор -n.
then
 echo "Строка \"string1\" не пустая."
else
 echo "Строка \"string1\" пустая."
fi
# Всё работает прекрасно.
# Квадратные скобки — [ ], без посторонней помощи определяют, что строка пустая.
# Тем не менее, хорошим тоном считается заключать строки в кавычки ("$string1").
# Как указывает Stephane Chazelas,
# if [ $string 1 ] один аргумент "]"
# if [ "$string 1" ] два аргумента, пустая "$string1" и "]"
echo
string1=initialized
if [ $string1 ] # Опять, попробуем строку без ничего.
then
 echo "Строка \"string1\" не пустая."
else
 echo "Строка \"string1\" пустая."
fi
# И снова получим верный результат.
# И опять-таки, лучше поместить строку в кавычки ("$string1"), поскольку...
echo
string1="a = b"
if [ $string1 ] # И снова, попробуем строку без ничего..
then
 echo "Строка \"string1\" не пустая."
else
 echo "Строка \"string1\" пустая."
fi
# Строка без кавычек даёт неверный результат!
exit 0
# Спасибо Florian Wisser, за предупреждение.

Пример: zmore

#!/bin/bash
#Просмотр gz-файлов с помощью утилиты 'more'
NOARGS=65
NOTFOUND=66
NOTGZIP=67
if [ $# -eq 0 ] # то же, что и: if [ -z "$1" ]
# $1 должен существовать, но может быть пустым: zmore "" arg2 arg3
then
 echo "Порядок использования: `basename $0` filename" >&2
 # Сообщение об ошибке на stderr.
 exit $NOARGS
 # Код возврата 65 (код ошибки).
fi
filename=$1
if [ ! -f "$filename" ] # Кавычки необходимы на тот случай, если имя файла содержит пробелы.
then
 echo "Файл $filename не найден!" >&2
 # Сообщение об ошибке на stderr.
exit $NOTFOUND
fi
if [ ${filename##*.} != "gz" ]
# Квадратные скобки нужны для выполнения подстановки значения переменной
then
 echo "Файл $1 не является gz-файлом!"
exit $NOTGZIP
fi
zcat $1 | more
# Используется утилита 'more' (очень похожа на 'less').
# Последние версии 'more' могут просматривать сжатые файлы.
# Можно вставить 'more' или 'less', если пожелаете.
exit $? # Сценарий возвращает код возврата, полученный по конвейеру.
# На самом деле команда "exit $?" не является обязательной,
# так как работа скрипта завершится здесь в любом случае.

Построение сложных условий проверки:

  • -a — логическое И (and)
exp1 -a exp2 возвращает true, если оба выражения, и exp1, и exp2 истинны.
  • -o — логическое ИЛИ (or)
exp1 -o exp2 возвращает true, если хотябы одно из выражений, exp1 или exp2 истинно.
Они похожи на операторы Bash && и ||, употребляемые в двойных квадратных скобках.
[[ condition1 && condition2 ]]
Операторы -o и -a употребляются совместно с командой test или внутри одинарных квадратных скобок.
if [ "$exp1" -a "$exp2" ]

Вложенные условные операторы if/then

Операторы проверки условий if/then могут быть вложенными друг в друга. Конечный результат будет таким же как если бы результаты всех проверок были объединены оператором &&.

if [ condition1 ]
then
 if [ condition2 ]
  then
   do-something # Только если оба условия "condition1" и "condition2" истинны.
 fi
fi

Проверка степени усвоения материала

Для запуска X-сервера может быть использован файл xinitrc. Этот файл содержит некоторое число операторов if/then. Ниже приводится отрывок из этого файла.

if [ -f $HOME/.Xclients ]; then
exec $HOME/.Xclients
 elif [ -f /etc/X11/xinit/Xclients ]; then
   exec /etc/X11/xinit/Xclients
else
# failsafe settings. Although we should never get here
# (we provide fallbacks in Xclients as well) it can't hurt.
 xclock -geometry 100x100-5+5 &
 xterm -geometry 80x50-50+150 &
 if [ -f /usr/bin/netscape -a -f /usr/share/doc/HTML/index.html ]; then
   netscape /usr/share/doc/HTML/index.html &
 fi
fi

Объясните действия условных операторов в вышеприведённом отрывке, затем просмотрите файл /etc/X11/xinit/xinitrc и проанализируйте его. Возможно вам придется обратиться к разделам, посвящённым grep, sed и регулярным выражениям.

Операции и смежные темы

Операторы

  • Присваивание

variable assignment
Инициализация переменной или изменение её значения.
= — Универсальный оператор присваивания, пригоден как для сравнения целых чисел, так и для сравнения строк. Пусть вас не смущает, что оператор присваивания ("="), по своему внешнему виду, совпадает с оператором сравнения (=).

# Здесь знак "=" выступает в качестве оператора сравнения
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

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

Пример: Наибольший общий делитель.

#!/bin/bash
# gcd.sh: поиск наибольшего общего делителя
# по алгоритму Эвклида
# Под "наибольшим общим делителем" (нод) двух целых чисел
# понимается наибольшее целое число, которое делит оба делимых без остатка.
# Алгоритм Эвклида выполняет последовательное деление.
# В каждом цикле,
# делимое <--- делитель
# делитель <--- остаток
# до тех пор, пока остаток не станет равным нулю (остаток = 0).
# The gcd = dividend, on the final pass.
# Замечательное описание алгоритма Эвклида можно найти
# на сайте Jim Loy, http://www.jimloy.com/number/euclids.htm.
# ------------------------------------------------------
# Проверка входных параметров
ARGS=2E_B
ADARGS=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 $2echo; 
echo "НОД чисел $1 и $2 = $dividend"; echo
# Упражнение :
# --------
# Вставьте дополнительную проверку входных аргументов,
# и предусмотрите завершение работы сценария с сообщением об ошибке, если
# входные аргументы не являются целыми числами.
exit 0
+= — "плюс-равно" (увеличивает значение переменной на заданное число).
let "var += 5"
значение переменной var будет увеличено на 5.

-= — "минус-равно" (уменьшение значения переменной на заданное число).

*= — "умножить-равно" (умножить значение переменной на заданное число, результат записать в переменную).
let "var *= 4"
значение переменной var будет увеличено в 4 раза.

/= — "слэш-равно" (уменьшение значения переменной в заданное число раз). %= — "процент-равно" (найти остаток от деления значения переменной на заданное число, результат записать в переменную). Арифметические операторы очень часто используются совместно с командами expr и let.

Пример: Арифметические операции.

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

Целые числа в Bash фактически являются длинными целыми (32-бит) со знаком, с диапазоном изменений от -2147483648 до 2147483647. Если в результате какой либо операции эти пределы будут превышены, то результат получится ошибочным.

#НЕАКТУАЛЕН для версии Bash  4.2!
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
              # ОШИБКА! (выход за границы диапазона)

Версия Bash 2.05b, поддерживает 64-битные целые числа. Bash ничего не знает о существовании чисел с плавающей запятой. Такие числа, из-за наличия символа десятичной точки, он воспринимает как строки.

a=1.5
let "b = $a + 1.3" # Ошибка.
# -bash: let: b = 1.5 + 1.3: syntax error: invalid arithmetic operator (error token is ".5 + 1.3")
echo "b = $b" # b=1
b=

Для работы с числами с плавающей запятой в сценариях можно использовать утилиту-калькулятор 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 ]] # То же верно
# Обратите внимание: оператор && не должен использоваться внутри [ ... ]!

Оператор &&, в зависимости от контекста, может так же использоваться в "И"-списках для построения составных команд.
|| — логическое ИЛИ (or)

if [ $condition1 ] || [ $condition2 ]
# То же самое, что: if [ $condition1 -o $condition2 ]
# Возвращает true, если хотя бы один из операндов истинен...
if [[ $condition1 || $condition2 ]] # Also works.
# Обратите внимание: оператор || не должен использоваться внутри [ ... ].

Bash производит проверку кода возврата КАЖДОГО из операндов в логических выражениях.

Пример: Построение сложных условий, использующих && и ||.

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

Числовые константы

Интерпретатор командной оболочки воспринимает числа как десятичные, в противном случае числу должен предшествовать специальный префикс, либо число должно быть записано в особой нотации. Числа, начинающиеся с символа 0, считаются восьмеричными. если числу предшествует префикс 0x, то число считается шестнадцатеричным. Число, в записи которого присутствует символ #, расценивается как запись числа с указанием основы счисления в виде ОСНОВА#ЧИСЛО.

Пример: Различные представления числовых констант.

#!/bin/bash
# numbers.sh: Различные представления числовых констант.
# Десятичное: по-умолчанию
let "dec = 32"
echo "десятичное число = $dec" # 32
# В общем-то ничего необычного.
# Восьмеричное: числа начинаются с '0' (нуля)
let "oct = 032"
echo "восьмеричное число = $oct" # 26
# Результат печатается в десятичном виде.
# --------- ------ -- -------
# Шестнадцатиричное: числа начинаются с '0x' или '0X'
let "hex = 0x32"
echo "шестнадцатиричное число = $hex" # 50
# Результат печатается в десятичном виде.
# Другие основы счисления: ОСНОВА#ЧИСЛО
# ОСНОВА должна быть между 2 и 64.
# для записи ЧИСЛА должен использоваться соответствующий ОСНОВЕ диапазон символов,
# см. ниже.
let "bin = 2#111100111001101"
echo "двоичное число = $bin" # 31181
let "b32 = 32#77"
echo "32-ричное число = $b32" # 231
let "b64 = 64#@_"
echo "64-ричное число = $b64" # 4094
#
# Нотация ОСНОВА#ЧИСЛО может использоваться на ограниченном
# диапазоне основ счисления (от 2 до 64)
# 10 цифр + 26 символов в нижнем регистре + 26 символов в верхнем регистре + @ + _
echo
echo $((36#zz)) $((2#10101010)) $((16#AF16)) $((53#1aA))
                    # 1295 170 44822 3375
# Важное замечание:
# --------------
# Использование символов, для записи числа, выходящих за диапазон,
# соответствующий ОСНОВЕ счисления
# будет приводить к появлению сообщений об ошибках.
let "bad_oct = 081"
# numbers.sh: let: oct = 081: value too great for base (error token is "081")
# Для записи восьмеричных чисел допускается использовать
#+ только цифры в диапазоне 0 - 7.
exit 0 # Спасибо Rich Bartell и Stephane Chazelas, за разъяснения.

Часть третья. Углублённый материал

К вопросу о переменных

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

Внутренние переменные

  • Встроенные переменные

$BASH — путь к исполняемому файлу Bash

bash$ echo $BASH
/bin/bash

$BASH_VERSINFO[n] — это массив, состоящий из 6 элементов, и содержащий информацию о версии Bash. Очень похожа на переменную $BASH_VERSION, описываемую ниже.

#!/bin/bash
# Информация о версии 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_VERSINFO — версия 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. Значение переменной $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
$HOSTNAME — Сетевое имя хоста устанавливается командой hostname во время исполнения инициализирующих сценариев при загрузке системы. Внутренняя переменная $HOSTNAME Bash получает своё значение посредством вызова функции gethostname().
$HOSTTYPE — тип машины Подобно $MACHTYPE, идентифицирует аппаратную архитектуру.

bash$ echo $HOSTTYPE
i686

$IFS — разделитель полей во вводимой строке (IFS — Internal Field Separator) Эта переменная управляет порядком выделения полей (задаёт символы-разделители) приразборе строки символов. По-умолчанию — пробельный символ (пробел, табуляция и перевод строки), но может быть изменён, например, для разбора строк, в которых отдельные поля разделены запятыми. Обратите внимание: при составлении содержимого переменной $*, Bash использует первый символ из $IFS для разделения аргументов.

bash$ echo $IFS | cat -vte
$
bash$ bash -c 'set w x y z; IFS=":-;"; echo "$*"'
w:x:y:z

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

Пример: $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 можно получить весьма неожиданные результаты. Начиная с версии 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.

bash$ echo $PATH
/bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin:/sbin:/usr/sbin

Инструкция PATH=${PATH}:/opt/bin добавляет каталог /opt/bin в конец текущего пути поиска. Иногда может оказаться целесообразным, внутри сценария, временно добавить какой-либо каталог к пути поиска. По завершении работы скрипта, эти изменения будут утеряны(вспомните о том, что невозможно изменить переменные окружения вызывающего процесса). Текущий "рабочий каталог", ./, обычно не включается в $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

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

tcsh% bash
bash$ who | grep nobody | sort
bash$ echo ${PIPESTATUS[*]}
0

Если поместить эти строки в сценарий и исполнить его, то будут выведены верные значения 0 1 0. Спасибо Wayne Pollock за замечания и предоставленный пример.
$PPID — Переменная $PPID хранит PID (идентификатор) родительского процесса. Сравните с командой pidof.
$PROMPT_COMMAND — Переменная хранит команду, которая используется непосредственно для вывода первичного приглашения к вводу — $PS1.
$PS1 — prompt, приглашение командной строки.
$PS2 — Вторичное приглашение командной строки, выводится тогда, когда от пользователя ожидается дополнительный ввод. Отображается как ">".
$PS3 — Третичное приглашение (prompt), выводится тогда, когда пользователь должен сделать выбор в операторе select.
$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 выше), может привести к автоматическому завершению сеанса работы. Начиная с версии 2.05b Bash, стало возможным использование $TMOUT в сценариях в комбинации с read.

# Работает только в сценариях Bash, начиная с версии 2.05b.
TMOUT=3 # Ожидание ввода не более трех секунд.
echo "Ваша любимая песня?"
echo "Вводите быстрее, у Вас только $TMOUT секунды на обдумывание!"
read song
if [ -z "$song" ]
then 
 song="(ответ не получен)" 
 # По-умолчанию.
fi
echo "Вам нравится $song."

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

Пример: Ограничения времени ожидания ввода.

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

Пример: Ещё один пример ограничения времени ожидания ввода от пользователя

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

Пример: Ограничение времени ожидания команды 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 доступна только для чтения.

Пример: Я — 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

Переменные $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
$# — Количество аргументов командной строки, или позиционных параметров
$* — Все аргументы (позиционные параметры), в виде одной строки (слова) "$*" — необходимо заключать в кавычки.
$@ — То же самое, что и "$*", но при этом каждый параметр представлен как отдельная строка(слово), т.е. параметры не подвергаются какой-либо интерпретации. "$@" -- необходимо заключать в кавычки.

Пример: Я — root?

#!/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, так ииз файла, имя которого передаётся сценарию из командной строки. Переменные $* и $@, в отдельных случаях, могут содержать противоречивую информацию! Это зависит от содержимого переменной $IFS.

Пример: Противоречия в переменных $* и $@

#!/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,
# Незначительные изменения внесены автором документа.

Различия между $@ и $* наблюдаются только тогда, когда они помещаются в двойные кавычки!

Пример: Содержимое $* и $@, когда переменная $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) Эта конструкция изначально была введена в 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 за предложенный пример.

$_ — Специальная переменная, содержит последний аргумент предыдущей команды.

Пример: Переменная "подчеркивание"

#!/bin/bash
echo $_ # /bin/bash
        # Для запуска сценария был вызван /bin/bash.
du >/dev/null # Подавление вывода.
echo $_ # du
ls -al >/dev/null # Подавление вывода.
echo $_ # -al (последний аргумент)
:
echo $_ # :

$? — Код возврата команды, функции или скрипта
$$ — PID самого процесса-сценария. Переменная $$ часто используется при генерации "уникальных" имён для временных файлов. Обычно это проще, чем вызов mktemp.

Работа со строками

Bash поддерживает на удивление большое количество операций над строками. К сожалению, этот раздел Bash испытывает недостаток унификации. Одни операции являются подмножеством операций подстановки параметров, а другие — совпадают с функциональностью команды Unix — expr. Это приводит к противоречиям в синтаксисе команд и перекрытию функциональных возможностей, не говоря уже о возникающей путанице.

Длина строки:

${#string}
expr length $string
expr "$string" : '.*'
stringZ=abcABC123ABCabcecho ${#stringZ} # 15
echo `expr length $stringZ` # 15
echo `expr "$stringZ" : '.*'`

Пример: Вставка пустых строк между параграфами в текстовом файле.

#!/bin/bash
# paragraph-space.sh
# Вставка пустых строк между параграфами в текстовом файле.
MINLEN=45 # Возможно потребуется изменить это значение.
# Строки, содержащие количество символов меньшее, чем $MINLEN
# принимаются за последнюю строку параграфа.
while read line # Построчное чтение файла от начала до конца...
do
 echo "$line" # Вывод строки.
 len=${#line}
 if [ "$len" -lt "$MINLEN" ]
  then echo # Добавление пустой строки после последней строки параграфа.
 fi
done
exit 0

Длина подстроки в строке (подсчёт совпадающих символов ведётся с начала строки):
expr match "$string" '$substring' , где $substring — регулярное выражение.
expr "$string" : '$substring' , где $substring — регулярное выражение.

stringZ=abcABC123ABCabc
#       |------|
echo `expr match "$stringZ" 'abc[A-Z]*.2'` # 8
echo `expr "$stringZ" : 'abc[A-Z]*.2'` # 8

Index:
Номер позиции первого совпадения в $string c первым символом в $substring:
expr index $string $substring

stringZ=abcABC123ABCabc
echo `expr index "$stringZ" C12` # 6
                                 # позиция символа C.
echo `expr index "$stringZ" 1c` # 3
# символ 'c' (в #3 позиции) совпал раньше, чем '1'.

Эта функция довольно близка к функции strchr() в языке C.
Извлечение подстроки:
${string:position}
Извлекает подстроку из $string, начиная с позиции $position.
Если строка $string — "*" или "@", то извлекается позиционный параметр (аргумент), с номером $position.
${string:position:length}
Извлекает $length символов из $string, начиная с позиции $position.

stringZ=abcABC123ABCabc
# 0123456789.....
# Индексация начинается с 0.
echo ${stringZ:0} # abcABC123ABCabc
echo ${stringZ:1} # bcABC123ABCabc
echo ${stringZ:7} # 23ABCabc
echo ${stringZ:7:3} # 23A
                    # Извлекает 3 символа.
# Возможна ли индексация с "правой" стороны строки?
echo ${stringZ:-4} # abcABC123ABCabc
# По-умолчанию выводится полная строка.
# Однако ...
echo ${stringZ:(-4)} # Cabc
echo ${stringZ: -4} # Cabc
# Теперь выводится правильно.
# Круглые скобки или дополнительный пробел "экранируют" параметр позиции.
# Спасибо Dan Jacobson, за разъяснения.

Если $string — "*" или "@", то извлекается до $length позиционных параметров (аргументов), начиная с $position.

echo ${*:2} # Вывод 2-го и последующих аргументов.
echo ${@:2} # То же самое.
echo ${*:2:3} # Вывод 3-х аргументов, начиная со 2-го.

expr substr $string $position $length
Извлекает $length символов из $string, начиная с позиции $position.

stringZ=abcABC123ABCabc # 123456789......# Индексация начинается с 1.
echo `expr substr $stringZ 1 2` # ab
echo `expr substr $stringZ 4 3` # ABC

expr match "$string" '\($substring\)'
Находит и извлекает первое совпадение $substring в $string, где $substring — это регулярное выражение.
expr "$string" : '\($substring\)'
Находит и извлекает первое совпадение $substring в $string, где $substring — это регулярное выражение.

stringZ=abcABC123ABCabc
# =======
echo `expr match "$stringZ" '\(.[b-c]*[A-Z]..[0-9]\)'` # abcABC1
echo `expr "$stringZ" : '\(.[b-c]*[A-Z]..[0-9]\)'` # abcABC1
echo `expr "$stringZ" : '\(.......\)'` # abcABC1
# Все вышеприведённые операции дают один и тот же результат.

expr match "$string" '.*\($substring\)'
Находит и извлекает первое совпадение $substring в $string, где $substring — это регулярное выражение. Поиск начинается с конца $string.
expr "$string" : '.*\($substring\)'
Находит и извлекает первое совпадение $substring в $string, где $substring — это регулярное выражение. Поиск начинается с конца $string.

stringZ=abcABC123ABCabc# ======
echo `expr match "$stringZ" '.*\([A-C][A-C][A-C][a-c]*\)'` # ABCabc
echo `expr "$stringZ" : '.*\(......\)'` # ABCabc

Удаление части строки:
${string#substring}
Удаление самой короткой, из найденных, подстроки $substring в строке $string. Поиск ведётся с начала строки.
${string##substring}
Удаление самой длинной, из найденных, подстроки $substring в строке $string. Поиск ведётся сначала строки.

stringZ=abcABC123ABCabc
#       |----|
#       |----------|
echo ${stringZ#a*C} # 123ABCabc
# Удаление самой короткой подстроки.
echo ${stringZ##a*C} # abc
# Удаление самой длинной подстроки.

${string%substring}
Удаление самой короткой, из найденных, подстроки $substring в строке $string. Поиск ведётся с конца строки.
${string%%substring}
Удаление самой длинной, из найденных, подстроки $substring в строке $string. Поиск ведётся с конца строки.

stringZ=abcABC123ABCabc
                     #||
#         |------------|
echo ${stringZ%b*c} # abcABC123ABCa
# Удаляется самое короткое совпадение. Поиск ведётся с конца $stringZ.
echo ${stringZ%%b*c} # a
# Удаляется самое длинное совпадение. Поиск ведётся с конца $stringZ.

Пример: Преобразование графических файлов из одного формата в другой, с изменением имени файла.

#!/bin/bash
# cvt.sh:
# Преобразование всех файлов в заданном каталоге,
# из графического формата MacPaint, в формат "pbm".
# Используется утилита "macptopbm", входящая в состав пакета "netpbm",
# который сопровождается Brian Henderson (bryanh@giraffe-data.com).
# Netpbm — стандартный пакет для большинства дистрибутивов Linux.
OPERATION=macptopbm
SUFFIX=pbm # Новое расширение файла.
if [ -n "$1" ]
then
 directory=$1 # Если каталог задан в командной строке при вызове сценария
else
 directory=$PWD # Иначе просматривается текущий каталог.
fi
# Все файлы в каталоге, имеющие расширение ".mac", считаются файлами
# формата MacPaint.
for file in $directory/* # Подстановка имен файлов.
do
 filename=${file%.*c} # Удалить расширение ".mac" из имени файла
                      # ( с шаблоном '.*c' совпадают все подстроки
                      # начинающиеся с '.' и заканчивающиеся 'c',
 $OPERATION $file > "$filename.$SUFFIX"
                      # Преобразование с перенаправлением в файл с новым именем
 rm -f $file          # Удаление оригинального файла после преобразования.
 echo "$filename.$SUFFIX" # Вывод на stdout.
done
exit 0
# Упражнение:
# --------
# Сейчас этот сценарий конвертирует *все* файлы в каталоге
# Измените его так, чтобы он конвертировал *только* те файлы,
# которые имеют расширение ".mac".

Замена подстроки:
${string/substring/replacement}
Замещает первое вхождение $substring строкой $replacement.
${string//substring/replacement}
Замещает все вхождения $substring строкой $replacement.

stringZ=abcABC123ABCabc
echo ${stringZ/abc/xyz}  # xyzABC123ABCabc
                         # Замена первой подстроки 'abc' строкой 'xyz'.
echo ${stringZ//abc/xyz} # xyzABC123ABCxyz
                         # Замена всех подстрок 'abc' строкой 'xyz'.

${string/#substring/replacement}
Подстановка строки $replacement вместо $substring, если строка $string начинается найденным соответствием. Поиск ведётся с начала строки $string.
${string/%substring/replacement}
Подстановка строки $replacement вместо $substring, если строка $string заканчивается найденным соответствием. Поиск ведётся с конца строки $string.

stringZ=abcABC123ABCabc
echo ${stringZ/#abc/XYZ} # XYZABC123ABCabc
                         # Поиск ведется с начала строки
echo ${stringZ/%abc/XYZ} # abcABC123ABCXYZ
                         # Поиск ведется с конца строки

Использование awk при работе со строками:
В качестве альтернативы, Bash-скрипты могут использовать средства awk при работе со строками.

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

#!/bin/bash
# substring-extraction.sh
String=23skidoo1
# 012345678 Bash
# 123456789 awk
# Обратите внимание на различия в индексации:
# Bash начинает индексацию с '0'.
# Awk начинает индексацию с '1'.
echo ${String:2:4} # с 3 позиции (0-1-2), 4 символа
                   # skid
# В эквивалент в awk: substr(string,pos,length).
echo | awk '
{ print substr("'"${String}"'",3,4) }' # skid
# Передача пустого "echo" по каналу в awk, означает фиктивный ввод,
# делая, тем самым, ненужным предоставление имени файла.
exit 0

Подстановка параметров

Работа с переменными и/или подстановка их значений
${parameter}
То же самое, что и $parameter, т.е. значение переменной parameter. В отдельных случаях, при возникновении неоднозначности интерпретации, корректно будет работать только такая форма записи: ${parameter}. Может использоваться для конкатенации (слияния) строковых переменных.

your_id=${USER}-on-${HOSTNAME}
echo "$your_id"
#
echo "Старый \$PATH = $PATH"
PATH=${PATH}:/opt/bin #Добавление /opt/bin в $PATH.
echo "Новый \$PATH = $PATH"


${parameter-default}, ${parameter:-default}
Если параметр отсутствует, то используется значение по-умолчанию.

echo ${username-`whoami`}
# Вывод результата работы команды `whoami`, если переменная $username не установлена.

Формы записи ${parameter-default} и ${parameter:-default} в большинстве случаев можно считать эквивалентными. Дополнительный символ : имеет значение только тогда, когда parameter определён, но имеет "пустое" (null)значение.

#!/bin/bash
username0=
# переменная username0 объявлена, но инициализирована "пустым" значением.
echo "username0 = ${username0-`whoami`}"
# Вывод после символа "=" отсутствует.
echo "username1 = ${username1-`whoami`}"
# Переменная username1 не была объявлена.
# Выводится имя пользователя, выданное командой `whoami`.
username2=
# переменная username2 объявлена, но инициализирована "пустым" значением.
echo "username2 = ${username2:-`whoami`}"
# Выводится имя пользователя, выданное командой `whoami`, поскольку
# здесь употребляется конструкция ":-" , а не "-".
exit 0

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

DEFAULT_FILENAME=generic.data
filename=${1:-$DEFAULT_FILENAME}
# Если имя файла не задано явно, то последующие операторы будут работать
# с файлом "generic.data".


${parameter=default},${parameter:=default}
Если значения параметров не заданы явно, то они принимают значения по-умолчанию. Оба метода задания значений по-умолчанию до определенной степени идентичны. Символ : имеет значение только когда $parameter был инициализирован "пустым" (null) значением, как показано выше.

echo ${username=`whoami`}
# Переменная "username" принимает значение, возвращаемое командой `whoami`.


${parameter+alt_value},${parameter:+alt_value}
Если параметр имеет какое-либо значение, то используется alt_value, иначе — null ("пустая" строка). Оба варианта до определенной степени идентичны. Символ : имеет значение только если parameter объявлен и "пустой".

echo "###### \${parameter+alt_value} ########"
echo
a=${param1+xyz}
echo "a = $a" # a =
param2=
a=${param2+xyz}
echo "a = $a" # a = xyz
param3=123
a=${param3+xyz}
echo "a = $a" # a = xyz
echo
echo "###### \${parameter:+alt_value} ########"
echo
a=${param4:+xyz}
echo "a = $a" # a =
param5=
a=${param5:+xyz}
echo "a = $a" # a =
# Вывод отличается от a=${param5+xyz}
param6=123
a=${param6+xyz}
echo "a = $a" # a = xyz


${parameter?err_msg},${parameter:?err_msg}
Если parameter инициализирован, то используется его значение, в противном случае — выводится err_msg. Обе формы записи можно, до определенной степени, считать идентичными. Символ : имеет значение только когда parameter инициализирован "пустым" значением.

Пример: Подстановка параметров и сообщения об ошибках.

#!/bin/bash
# Проверка отдельных переменных окружения.
# Если переменная, к примеру $USER, не установлена,
# то выводится сообщение об ошибке.
: ${HOSTNAME?} ${USER?} ${HOME?} ${MAIL?}
echo
echo "Имя машины: $HOSTNAME."
echo "Ваше имя: $USER."
echo "Ваш домашний каталог: $HOME."
echo "Ваш почтовый ящик: $MAIL."
echo
echo "Если перед Вами появилось это сообщение,"
echo "то это значит, что все критические переменные окружения установлены."
echo
echo
# ------------------------------------------------------
# Конструкция ${variablename?} так же выполняет проверку
# наличия переменной в сценарии.
ThisVariable=Value-of-ThisVariable
# Обратите внимание, в строковые переменные могут быть записаны
# символы, которые запрещено использовать в именах переменных.
: ${ThisVariable?}
echo "Value of ThisVariable is $ThisVariable".
echo
echo
: ${ZZXy23AB?"Переменная ZZXy23AB не инициализирована."}
# Если ZZXy23AB не инициализирована,
# то сценарий завершается с сообщением об ошибке.
# Текст сообщения об ошибке можно задать свой.
# : ${ZZXy23AB?"Переменная ZZXy23AB не инициализирована."}
# То же самое: dummy_variable=${ZZXy23AB?}
#              dummy_variable=${ZZXy23AB?"Переменная ZXy23AB не инициализирована."}
#              echo ${ZZXy23AB?} >/dev/null
echo "Это сообщение не будет напечатано, поскольку сценарий завершится раньше."
HERE=0
exit $HERE # Сценарий завершит работу не здесь.

Пример: Подстановка параметров и сообщение о "порядке использования".

#!/bin/bash
# usage-message.sh
: ${1?"Порядок использования: $0 ARGUMENT"}
# Сценарий завершит свою работу здесь, если входные аргументы отсутствуют,
# со следующим сообщением.
# usage-message.sh: 1: Порядок использования: usage-message.sh ARGUMENT
echo "Эти две строки появятся, только когда задан аргумент в командной строке."
echo "Входной аргумент командной строки = \"$1\""
exit 0 # Точка выхода находится здесь, только когда задан аргумент командной строки.
# Проверьте код возврата в обеих случаях, с и без аргумента командной строки.
# Если аргумент задан, то код возврата будет равен 0.
# Иначе — 1.

Подстановка параметров и/или экспансия.
Следующие выражения могут служить дополнениями оператора match команды expr, применяемой к строкам. Как правило, они используются при разборе имён файлов и каталогов.
Длина переменной/Удаление подстроки.
${#var}
String length (число символов в переменной $var). В случае массивов, команда ${#array} возвращает длину первого элемента массива.
Исключения:

  • ${#*} и ${#@} возвращает количество аргументов (позиционных параметров).
  • Для массивов, ${#array[*]} и ${#array[@]} возвращает количество элементов в массиве.

Пример: Длина переменной.

#!/bin/bash
# length.sh
E_NO_ARGS=65if [ $# -eq 0 ] # Для работы скрипта необходим хотя бы один входной параметр.
then
 echo "Вызовите сценарий с одним или более параметром командной строки."
 exit $E_NO_ARGS
fi
var01=abcdEFGH28ij
echo "var01 = ${var01}"
echo "Length of var01 = ${#var01}"
echo "Количество входных параметров = ${#@}"
echo "Количество входных параметров = ${#*}"
exit 0


${var#Pattern},${var##Pattern}
Удаляет из переменной $var наименьшую/наибольшую подстроку, совпадающую с шаблоном $Pattern. Поиск ведётся с начала строки $var.

# Функция из сценария "days-between.sh".
95
# Удаляет нули, стоящие в начале аргумента-строки.
strip_leading_zero () # Ведущие нули, которые могут находиться в номере дня/месяца, лучше удалить
{                     # В противном случае Bash будет интерпретировать числа как восьмеричные 
return=${1#0}         # "1" — это аргумент "$1".
}                     # "0" — это то, что удаляется из "$1" — ведущие нули.

Manfred Schwarb предложил более сложный вариант вышеприведенного кода:

strip_leading_zero2 () # Удалить ведущие нули.
{
shopt -s extglob               # Переход в режим расширенной подстановки.
local val=${1##+(0)}           # Поиск самой длинной последовательности нулей.
shopt -u extglob               # выход из режима расширенной подстановки.
_strip_leading_zero2=${val:-0}
                               # Если был введен 0, то возвращается 0, а не "".
}

Другой пример:

echo `basename $PWD`   # Имя текущего рабочего каталога.
echo "${PWD##*/}"      # Имя текущего рабочего каталога.
echo
echo `basename $0`     # Имя файла-сценария.
echo $0                # Имя файла-сценария.
echo "${0##*/}"        # Имя файла-сценария.
echo
filename=test.data
echo "${filename##*.}" # data
                       # Расширение файла.


${var%Pattern},${var%%Pattern}
Удаляет из переменной $var наименьшую/наибольшую подстроку, совпадающую с шаблоном $Pattern. Поиск ведётся с конца строки $var.Bash версии 2 имеет ряд дополнительных возможностей.

Пример: Поиск по шаблону в подстановке параметров.

#!/bin/bash
# Поиск по шаблону в операциях подстановки параметров # ## % %%.
var1=abcd12345abc6789
pattern1=a*c # * (символ шаблона), означает любые символы между a и c.
echo
echo "var1 = $var1" # abcd12345abc6789
echo "var1 = ${var1}" # abcd12345abc6789 (альтернативный вариант)
echo "Число символов в ${var1} = ${#var1}"
echo "pattern1 = $pattern1" # a*c (между 'a' и 'c' могут быть любые символы)
echo
echo '${var1#$pattern1} =' "${var1#$pattern1}" # d12345abc6789
# Наименьшая подстрока, удаляются первые 3 символа abcd12345abc6789
                                  ^^^^^^           |-|
echo '${var1##$pattern1} =' "${var1##$pattern1}" # 6789
# Наибольшая подстрока, удаляются первые 12 символов abcd12345abc6789
#                                           ^^^^^^   |----------|
echo; echo
pattern2=b*9 # все, что между 'b' и '9'
echo "var1 = $var1" # abcd12345abc6789
echo "pattern2 = $pattern2"
echo
echo '${var1%pattern2} =' "${var1%$pattern2}" # abcd12345a
# Наименьшая подстрока, удаляются последние 6 символов abcd12345abc6789
#                                 ^^^^^^^^^                      |----|
echo '${var1%%pattern2} =' "${var1%%$pattern2}" # a
# Наибольшая подстрока, удаляются последние 12 символов abcd12345abc6789
#                                 ^^^^^^^^^              |-------------|
# Запомните, # и ## используются для поиска с начала строки,
# % и %% используются для поиска с конца строки.
echo
exit 0

Пример: Изменение расширений в именах файлов.

#!/bin/bash
# rfe
# ---
# Изменение расширений в именах файлов.
#
# rfe old_extension new_extension
# Пример:
# Изменить все расширения *.gif в именах файлов на *.jpg, в текущем каталоге
# rfe gif jpg
ARGS=2
E_BADARGS=65
if [ $# -ne "$ARGS" ]
then
 echo "Порядок использования: `basename $0` old_file_suffix new_file_suffix"
 exit $E_BADARGS
fi
for filename in *.$1
# Цикл прохода по списку имен файлов, имеющих расширение равное первому аргументу.
do
mv $filename ${filename%$1}$2
# Удалить первое расширение и добавить второе,
done
exit 0


Подстановка значений переменных / Замена подстроки
Эти конструкции перекочевали в Bash из ksh.
${var:pos}
Подставляется значение переменной var, начиная с позиции pos.
${var:pos:len}
Подставляется значение переменной var, начиная с позиции pos, не более len символов.
${var/Pattern/Replacement}
Первое совпадение с шаблоном Pattern, в переменной var замещается подстрокой Replacement. Если подстрока Replacement отсутствует, то найденное совпадение будет удалено.
${var//Pattern/Replacement}
Глобальная замена. Все найденные совпадения с шаблоном Pattern, в переменной var, будут замещены подстрокой Replacement. Как и в первом случае, если подстрока Replacement отсутствует, то все найденные совпадения будут удалены.

Пример: Поиск по шаблону при анализе произвольных строк.

#!/bin/bash
var1=abcd-1234-defg
echo "var1 = $var1"
t=${var1#*-*}
echo "var1 (все, от начала строки по первый символ \"-\", включительно, удаляется) = $t"
# t=${var1#*-} то же самое,
# поскольку оператор # ищет кратчайшее совпадение,
# а * соответствует любым предшествующим символам, включая пустую строку.
# (Спасибо S. C. за разъяснения.)
t=${var1##*-*}
echo "Если var1 содержит \"-\", то возвращается пустая строка... var1 = $t"
t=${var1%*-*}
echo "var1 (все, начиная с последнего \"-\" удаляется) = $t"
echo
# -------------------------------------------
path_name=/home/bozo/ideas/thoughts.for.today
# -------------------------------------------
echo "path_name = $path_name"
t=${path_name##/*/}
echo "Из path_name удален путь к файлу = $t"
# В данном случае, тот эе эффект можно получить так: t=`basename $path_name`
# t=${path_name%/}; t=${t##*/} более общее решение,
# но имеет некоторые ограничения.
# Если $path_name заканчивается символом перевода строки, то `basename $path_name` не будет работать,
# но для данного случая вполне применимо.
# (Спасибо S.C.)
t=${path_name%/*.*}
# Тот же эффект дает t=`dirname $path_name`
echo "Из path_name удалено имя файла = $t"
# Этот вариант будет терпеть неудачу в случаях: "../", "/foo////", # "foo/", "/".
# Удаление имени файла, особенно когда его нет,
# использование dirname имеет свои особенности.
# (Спасибо S.C.)
echo
t=${path_name:11}
echo "Из $path_name удалены первые 11 символов = $t"
t=${path_name:11:5}
echo "Из $path_name удалены первые 11 символов, выводится 5 символов = $t"
echo
t=${path_name/bozo/clown}
echo $path_name подстрока \"bozo\" заменена на \"clown\" = $t"
t=${path_name/today/}
echo $path_name подстрока \"today\" удалена = $t"
t=${path_name//o/O}
echo $path_name все символы \"o\" переведены в верхний регистр, = $t"
t=${path_name//o/}
echo "Из $path_name удалены все символы \"o\" = $t"
exit 0


${var/#Pattern/Replacement}
Если в переменной var найдено совпадение с Pattern, причём совпадающая подстрока расположена в начале строки (префикс), то оно заменяется на Replacement. Поиск ведётся сначала строки.
${var/%Pattern/Replacement}
Если в переменной var найдено совпадение с Pattern, причём совпадающая подстрока расположена в конце строки (суффикс), то оно заменяется на Replacement. Поиск ведётся с конца строки.

Пример: Поиск префиксов и суффиксов с заменой по шаблону.

#!/bin/bash
# Поиск с заменой по шаблону.
v0=abc1234zip1234abc # Начальное значение переменной.
echo "v0 = $v0"      # abc1234zip1234abc
echo
# Поиск совпадения с начала строки.
v1=${v0/#abc/ABCDEF} # abc1234zip1234abc
                     # |-|
echo "v1 = $v1"      # ABCDE1234zip1234abc
                     # |---|
# Поиск совпадения с конца строки.
v2=${v0/%abc/ABCDEF} # abc1234zip123abc
#                                   |-|
echo "v2 = $v2"      # abc1234zip1234ABCDEF
#                                    |----|
echo
# ----------------------------------------------------
# Если совпадение находится не с начала/конца строки,
# то замена не производится.
# ----------------------------------------------------
v3=${v0/#123/000}    # Совпадение есть, но не в начале строки.
echo "v3 = $v3"      # abc1234zip1234abc
                     # ЗАМЕНА НЕ ПРОИЗВОДИТСЯ!
v4=${v0/%123/000}    # Совпадение есть, но не в конце строки.
echo "v4 = $v4"      # abc1234zip1234abc
                     # ЗАМЕНА НЕ ПРОИЗВОДИТСЯ!
exit 0


${!varprefix*},${!varprefix@}
Поиск по шаблону всех, ранее объявленных переменных, имена которых начинаются с varprefix.

xyz23=whatever
xyz24=
a=${!xyz*}         # Подстановка имен объявленных переменных, которые начинаются с "xyz".
echo "a = $a"      # a = xyz23 xyz24
a=${!xyz@}         # То же самое.
echo "a = $a"      # a = xyz23 xyz24
# Эта возможность была добавлена в Bash, в версии 2.04.

Объявление переменных: declare и typeset

Инструкции declare и typeset являются встроенными инструкциями (они абсолютно идентичны друг другу и являются синонимами) и предназначена для наложения ограничений на переменные. Это очень слабая попытка контроля над типами, которая имеется во многих языках программирования. Инструкция declare появилась в Bash, начиная с версии 2. Кроме того, инструкция typeset может использоваться и в ksh-сценариях. Ключи инструкций declare/typeset
-r readonly (только для чтения)
declare -r var1
(declare -r var1 аналогично объявлению readonly var1).
Это грубый эквивалент констант (const) в языке C. Попытка изменения таких переменных завершается сообщением об ошибке.
-i integer
declare -i number

# Сценарий интерпретирует переменную "number" как целое число.
number=3
echo "number = $number"  # number = 3
number=three
echo "number = $number"  # number = 0
# Строка "three" интерпретируется как целое число.

Допускается выполнение некоторых арифметических операций над переменными, объявленными как integer, не прибегая к инструкциям expr или let.

n=6/3
echo "n = $n" # n = 6/3
declare -i n
n=6/3
echo "n = $n" # n = 2

-a array
declare -a indices
Переменная indices объявляется массивом.
-f functions
declare -f
Инструкция declare -f, без аргументов, приводит к выводу списка ранее объявленных функций в сценарии.
declare -f function_name
Инструкция declare -f function_name выводит имя функции function_name, если она была объявлена ранее.
-x export
declare -x var3
Эта инструкция объявляет переменную, как доступную для экспорта.
-x var=$value
declare -x var3=373
Инструкция declare допускает совмещение объявления и присваивания значения переменной одновременно.

Пример: Объявление переменных с помощью инструкции declare.

#!/bin/bash
func1 ()
{
echo Это функция.
}
declare -f               # Список функций, объявленных выше.
echo
declare -i var1          # var1 — целочисленная переменная.
var1=2367
echo "переменная var1 объявлена как $var1"
var1=var1+1              # Допустимая арифметическая операция над целочисленными переменными.
echo "переменная var1 увеличена на 1 = $var1."
                         # Допустимая операция для целочисленных переменных
echo "Возможно ли записать дробное число 2367.1 в var1?"
var1=2367.1              # Сообщение об ошибке, переменная не изменяется.
echo "значение переменной var1 осталось прежним = $var1"
echo
declare -r var2=13.36    # инструкция 'declare' допускает установку свойств переменной
                         # и одновременно присваивать значение.
echo "var2 declared as $var2" # Допускается ли изменять значение readonly переменных?
var2=13.37               # Сообщение об ошибке и завершение работы сценария.
echo "значение переменной var2 осталось прежним $var2" # Эта строка никогда не будет выполнена.
exit 0                   # Сценарий завершит работу выше.

Косвенные ссылки на переменные

Предположим, что значение одной переменной — есть имя второй переменной. Возможно ли получить значение второй переменной через обращение к первой?
Например, пусть a=letter_of_alphabet и letter_of_alphabet=z, тогда вопрос будет звучать так:
"Возможно ли получить значение z, обратившись к переменной a?".
В действительности это возможно и это называется косвенной ссылкой. Для этого необходимо прибегнуть к несколько необычной нотации eval var1=\$$var2.

Пример: Косвенные ссылки.

#!/bin/bash
# Косвенные ссылки на переменные.
a=letter_of_alphabet
letter_of_alphabet=z
echo
# Прямое обращение к переменной.
echo "a = $a"
# Косвенное обращение к переменной.
eval a=\$$a
echo "А теперь a = $a"
echo
# Теперь попробуем изменить переменную, на которую делается ссылка.
t=table_cell_3
table_cell_3=24
echo "\"table_cell_3\" = $table_cell_3"
echo -n "разыменование (получение ссылки) \"t\" = "; eval echo \$$t
# В данном, простом, случае,
# eval t=\$$t; echo "\"t\" = $t"
# даёт тот же результат (почему?).
echo
t=table_cell_3
NEW_VAL=387
table_cell_3=$NEW_VAL
echo "Значение переменной \"table_cell_3\" изменено на $NEW_VAL."
echo "Теперь \"table_cell_3\" = $table_cell_3"
echo -n "разыменование (получение ссылки) \"t\" = "; eval echo \$$t
# инструкция "eval" принимает два аргумента "echo" и "\$$t" (назначает равным $table_cell_3)
echo
# (Спасибо S.C. за разъяснения.)
# Ещё один способ — нотация ${!t}, будет обсуждаться в разделе "Bash, версия 2".
exit 0

Пример: Передача косвенных ссылок в awk.

#!/bin/bash
# Другая версия сценария "column totaler"
# который суммирует заданную колонку (чисел) в заданном файле.
# Здесь используются косвенные ссылки.
ARGS=2
E_WRONGARGS=65
if [ $# -ne "$ARGS" ] # Проверка количества входных аргументов.
then
 echo "Порядок использования: `basename $0` filename column-number"
 exit $E_WRONGARGS
fi
filename=$1
column_number=$2
#===== До этой строки идентично первоначальному варианту сценария =====#
# Многострочные скрипты awk вызываются конструкцией awk ' ..... '
# Начало awk-сценария.
# ------------------------------------------------
awk "
{ total += \$${column_number} # косвенная ссылка
}
END {  
      print total
    }
   " "$filename"
# ------------------------------------------------
# Конец awk-сценария.
# Косвенные ссылки делают возможным бесконфликтное
# обращение к переменным shell внутри вложенных сценариев awk.
# Спасибо Stephane Chazelas.
exit 0

Такой метод обращения к переменным имеет свои особенности. Если переменная, на которую делается ссылка, меняет свое значение, то переменная которая ссылается, должна быть должным образом разыменована, т.е. должна быть выполнена операция получения ссылки, как это делается в примере выше. К счастью, нотация ${!variable}, введённая в Bash, начиная с версии 2 позволяет выполнять косвенные ссылки более интуитивно понятным образом.

$RANDOM: генерация псевдослучайных целых чисел

$RANDOM — внутренняя функция Bash (не константа), которая возвращает псевдослучайные целые числа в диапазоне 0—32767. Функция $RANDOM не должна использоваться для генерации ключей шифрования.

Пример: Генерация случайных чисел.

#!/bin/bash
# $RANDOM возвращает различные случайные числа при каждом обращении к ней.
# Диапазон изменения: 0—32767 (16-битовое целое со знаком).
MAXCOUNT=10
count=1
echo
echo "$MAXCOUNT случайных чисел:"
echo "-----------------"
while [ "$count" -le $MAXCOUNT ] # Генерация 10 ($MAXCOUNT) случайных чисел.
do
 number=$RANDOM
 echo $number
 let "count += 1" # Нарастить счетчик.
done
echo "-----------------"
# Если вам нужны случайные числа не превышающие определенного числа,
# воспользуйтесь оператором деления по модулю (остаток от деления).
RANGE=500
echo
number=$RANDOM
let "number %= $RANGE"
echo "Случайное число меньше $RANGE —— $number"
echo
# Если вы желаете ограничить диапазон "снизу",
# то просто производите генерацию псевдослучайных чисел в цикле до тех пор,
# пока не получите число большее нижней границы.
FLOOR=200
number=0 # инициализация
while [ "$number" -le $FLOOR ]
do
 number=$RANDOM
done
echo "Случайное число, большее $FLOOR —— $number"
echo
# Эти два способа могут быть скомбинированы.
number=0 #initialize
while [ "$number" -le $FLOOR ]
do
 number=$RANDOM
 let "number %= $RANGE" # Ограничение "сверху" числом $RANGE.
done
echo "Случайное число в диапазоне от $FLOOR до $RANGE —— $number"
echo
# Генерация случайных "true" и "false" значений.
BINARY=2
number=$RANDOMT=1
let "number %= $BINARY"
# let "number >>= 14" даёт более равномерное распределение
# (сдвиг вправо смещает старший бит на нулевую позицию, остальные биты
обнуляются).
if [ "$number" -eq $T ]
then
 echo "TRUE"
else
 echo "FALSE"
fi
echo
# Можно имитировать бросание 2-х игровых кубиков.
SPOTS=7 # остаток от деления на 7 дает диапазон 0 - 6.
ZERO=0
die1=0
die2=0
# Кубики "выбрасываются" раздельно.
while [ "$die1" -eq $ZERO ] # Пока на "кубике" ноль.
do
 let "die1 = $RANDOM % $SPOTS" # Имитировать бросок первого кубика.
done
while [ "$die2" -eq $ZERO ]
do
 let "die2 = $RANDOM % $SPOTS" # Имитировать бросок второго кубика.
done
let "throw = $die1 + $die2"
echo "Результат броска кубиков = $throw"
echo
exit 0

Пример: Выбор случайной карты из колоды.

#!/bin/bash
# pick-card.sh
# Пример выбора случайного элемента массива.
# Выбор случайной карты из колоды.
Suites="Треф 
Бубей 
Червей 
Пик"
Denominations="2
3
4
5
6
7
8
9
10
Валет
Дама
Король
Туз"
# Инициализация массивов.
suite=($Suites) 
denomination=($Denominations)
num_suites=${#suite[*]} # Количество элементов массивов.
num_denominations=${#denomination[*]}
echo -n "${denomination[$((RANDOM%num_denominations))]} "
echo ${suite[$((RANDOM%num_suites))]}
# $bozo sh pick-cards.sh
# Валет Треф
# Спасибо "jipe," за пояснения по работе с $RANDOM.
exit 0

Jipe подсказал ещё один способ генерации случайных чисел из заданного диапазона.

# Генерация случайных чисел в диапазоне 6—30.
rnumber=$((RANDOM%25+6))
# Генерируется случайное число из диапазона 6—30,
# но при этом число должно делиться на 3 без остатка.
rnumber=$(((RANDOM%30/3+1)*3))
# Примечательно, если $RANDOM возвращает 0
# то это приводит к возникновению ошибки.
# Упражнение: Попробуйте разобраться с выражением самостоятельно.

Bill Gradwohl предложил усовершенствованную формулу генерации положительных псевдослучайных чисел в заданном диапазоне.

rnumber=$(((RANDOM%(max-min+divisibleBy))/divisibleBy*divisibleBy+min))

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

Пример: Псевдослучайное число из заданного диапазона.

#!/bin/bash
# random-between.sh
# Псевдослучайное число из заданного диапазона.
# Автор: Bill Gradwohl,
# незначительные изменения внесены автором документа.
# Используется с разрешения автора сценария.
randomBetween() {
# Генерация положительных и отрицательных псевдослучайных чисел,
# в диапазоне от $min до $max,
# которые кратны числу $divisibleBy.
#
# Bill Gradwohl — Oct 1, 2003
syntax() {
# Вложенная функция.
echo
echo "Порядок вызова: randomBetween [min] [max] [multiple]"
echo
echo "Функция ожидает до 3-х входных аргументов, но они не являются обязательными."
echo "min — нижняя граница диапазона"
echo "max — верхняя граница диапазона"
echo "multiple — делитель, на который должен делиться результат без остатка."
echo
echo "Если какой либо из параметров отсутствует, по-умолчанию принимаются значения: 0 32767 1"
echo "В случае успеха функция возвращает 0, иначе — 1"
echo "и это сообщение о порядке вызова."
echo "Результат возвращается в глобальной переменной randomBetweenAnswer"
echo "Входные параметры, имеющие отрицательные значения, обрабатываются корректно."
}
local min=${1:-0}
local max=${2:-32767}
local divisibleBy=${3:-1}
# При отсутствии какого-либо из входных параметров, они принимают значения по-умолчанию.
local x
local spread
# Делитель должен быть положительным числом.
[ ${divisibleBy} -lt 0 ] && divisibleBy=$((0-divisibleBy))
# Проверка корректности входных параметров.
if [ $# -gt 3 -o ${divisibleBy} -eq 0 -o ${min} -eq ${max} ]; then
 syntax
 return 1
fi
# Если min больше чем max, то поменять их местами.
if [ ${min} -gt ${max} ]; then
 # Поменять местами.
 x=${min}
 min=${max}
 max=${x}
fi
# Если min не делится без остатка на $divisibleBy,
# то привести его к ближайшему подходящему числу из заданного диапазона.
if [ $((min/divisibleBy*divisibleBy)) -ne ${min} ]; then
 if [ ${min} -lt 0 ]; then
  min=$((min/divisibleBy*divisibleBy))
 else
  min=$((((min/divisibleBy)+1)*divisibleBy))
 fi
fi
# Если max не делится без остатка на $divisibleBy,
# то привести его к ближайшему подходящему числу из заданного диапазона.
if [ $((max/divisibleBy*divisibleBy)) -ne ${max} ]; then
 if [ ${max} -lt 0 ]; then
  max=$((((max/divisibleBy)-1)*divisibleBy))
 else
  max=$((max/divisibleBy*divisibleBy))
 fi
fi
# ---------------------------------------------------------------------
# А теперь собственно нахождение псевдослучайного числа.
# Обратите внимание: чтобы получить псевдослучайное число в конце диапазона
# необходимо рассматривать диапазон от 0 до
# abs(max-min)+divisibleBy, а не abs(max-min)+1.
# Этим превышением верхней границы диапазона можно пренебречь
# поскольку эта новая граница никогда не будет достигнута.
 
# Если использовать при вычислении формулу abs(max-min)+1,
# то будут получаться вполне корректные значения, но при этом,
# возвращаемые значения будут значительно ниже
# верхней границы диапазона.
# ---------------------------------------------------------------------
spread=$((max-min))
[ ${spread} -lt 0 ] && spread=$((0-spread))
let spread+=divisibleBy
randomBetweenAnswer=$(((RANDOM%spread)/divisibleBy*divisibleBy+min))
return 0
}
# Проверка функции.
min=-14
max=20
divisibleBy=3
# Создадим массив, который будет содержать счетчики встречаемости каждого из чисел
# в заданном диапазоне.
declare -a answer
minimum=${min}
maximum=${max}
if [ $((minimum/divisibleBy*divisibleBy)) -ne ${minimum} ]; then
 if [ ${minimum} -lt 0 ]; then
  minimum=$((minimum/divisibleBy*divisibleBy))
 else
  minimum=$((((minimum/divisibleBy)+1)*divisibleBy))
 fi
fi
 
# Если max не делится без остатка на $divisibleBy,
# то привести его к ближайшему подходящему числу из заданного диапазона.
if [ $((maximum/divisibleBy*divisibleBy)) -ne ${maximum} ]; then
 if [ ${maximum} -lt 0 ]; then
  maximum=$((((maximum/divisibleBy)-1)*divisibleBy))
 else
  maximum=$((maximum/divisibleBy*divisibleBy))
 fi
fi
 
# Необходимое условие при работе с массивами —
# индекс массива должен быть положительным числом,
# поэтому введена дополнительная переменная displacement, которая
# гарантирует положительность индексов.
displacement=$((0-minimum))
for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
 answer[i+displacement]=0
done
# Цикл с большим количеством итераций, чтобы посмотреть - что мы получаем.
loopIt=1000 # Автор сценария предложил 100000 итераций,
            # но в этом случае цикл работает чересчур долго.
for ((i=0; i<${loopIt}; ++i)); do
# Обратите внимание: числа min и max передаются функции в обратном порядке,
# чтобы продемонстрировать, что функция обрабатывает их корректно.
 randomBetween ${max} ${min} ${divisibleBy}
# Вывод сообщения об ошибке, если функция вернула некорректное значение.
 [ ${randomBetweenAnswer} -lt ${min} -o ${randomBetweenAnswer} -gt ${max} ] && echo \
 "Выход за границы диапазона MIN..MAX" - ${randomBetweenAnswer}!
 [ $((randomBetweenAnswer%${divisibleBy})) -ne 0 ] && echo Число не делится на заданный делитель без остатка - ${randomBetweenAnswer}!
# Записать полученное число в массив.
 answer[randomBetweenAnswer+displacement]=$((answer[randomBetweenAnswer+displacement]+1))
done
# Проверим полученные результаты
for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
 [ ${answer[i+displacement]} -eq 0 ] && echo "Число $i не было получено ни разу." || echo \
 "Число ${i} встречено ${answer[i+displacement]} раз."
done
exit 0

Насколько случайны числа, возвращаемые функцией $RANDOM? Лучший способ оценить"случайность" генерируемых чисел — это написать сценарий, который будет имитировать бросание игрального кубика достаточно большое число раз, а затем выведет количество выпадений каждой из граней...

Пример: Имитация бросания кубика с помощью RANDOM.

#!/bin/bash
# Случайные ли числа возвращает RANDOM?
RANDOM=$$ # Инициализация генератора случайных чисел числом PID процесса-сценария.
PIPS=6 # Кубик имеет 6 граней.
MAXTHROWS=600 # Можете увеличить, если не знаете куда девать свое время.
throw=0 # Счётчик бросков.
zeroes=0 # Обнулить счётчики выпадения отдельных граней.
ones=0 # т.к. неинициализированные переменные - "пустые", и не равны нулю!.
twos=0
threes=0
fours=0
fives=0
sixes=0
 
print_result ()
{
 echo
 echo "единиц = $ones"
 echo "двоек = $twos"
 echo "троек = $threes"
 echo "четвёрок = $fours"
 echo "пятёрок = $fives"
 echo "шестёрок = $sixes"
 echo
}
 
update_count()
{
 case "$1" in
  0) let "ones += 1";; # 0 соответствует грани "1".
  1) let "twos += 1";; # 1 соответствует грани "2", и так далее
  2) let "threes += 1";;
  3) let "fours += 1";;
  4) let "fives += 1";;
  5) let "sixes += 1";;
 esac
}
 
echo
 
while [ "$throw" -lt "$MAXTHROWS" ]
do
 let "die1 = RANDOM % $PIPS"
 update_count $die1
 let "throw += 1"
done
 
print_result
# Количество выпадений каждой из граней должно быть примерно одинаковым, если считать RANDOM достаточно случайным.
# Для $MAXTHROWS = 600, каждая грань должна выпасть примерно 100 раз (плюс-минус 20).
#
# Имейте ввиду, что RANDOM - это генератор ПСЕВДОСЛУЧАЙНЫХ чисел,
# Упражнение:
# ---------------
# Перепишите этот сценарий так, чтобы он имитировал 1000 бросков монеты.
# Перепишите этот сценарий так, чтобы он имитировал 1000 бросков монеты.
# На каждом броске возможен один из двух вариантов выпадения — "ОРЁЛ" или "РЕШКА".
exit 0

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

Пример: Переустановка RANDOM.

#!/bin/bash
# seeding-random.sh: Переустановка переменной RANDOM.
MAXCOUNT=25 # Длина генерируемой последовательности чисел.
 
random_numbers ()
{
 count=0
 while [ "$count" -lt "$MAXCOUNT" ]
 do
  number=$RANDOM
  echo -n "$number "
  let "count += 1"
 done
}
 
echo; echo
RANDOM=1 # Переустановка начального числа генератора случайных чисел RANDOM.
random_numbers
echo; echo
RANDOM=1 # То же самое начальное число...
random_numbers # ...в результате получается та же последовательность чисел.
               # В каких случаях может оказаться полезной генерация совпадающих серий?
echo; echo
RANDOM=2 # Ещё одна попытка, но с другим начальным числом...
random_numbers # получим другую последовательность.
echo; echo
# RANDOM=$$ в качестве начального числа выбирается PID процесса-сценария.
# Вполне допустимо взять в качестве начального числа результат работы команд 'time' или 'date'.
# Немного воображения...
SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
# Псевдослучайное число забирается
# из системного генератора псевдослучайных чисел /dev/urandom,
# затем конвертируется в восьмеричное число командой "od",
# и наконец "awk" возвращает единственное число для переменной SEED.
RANDOM=$SEED
random_numbers
echo; echo
exit 0

Системный генератор /dev/urandom даёт последовательность псевдослучайных чисел с более равномерным распределением, чем $RANDOM. Команда dd if=/dev/urandom of=targetfile bs=1count=XX создаёт файл, содержащий последовательность псевдослучайных чисел. Однако, эти числа требуют дополнительной обработки, например с помощью команды od (этот приём используется в примере выше) или dd. Есть и другие способы генерации псевдослучайных последовательностей в сценариях. Awk имеет для этого достаточно удобные средства.

Пример: Получение псевдослучайных чисел с помощью awk.

#!/bin/bash
# random2.sh: Генерация псевдослучайных чисел в диапазоне 0 - 1.
# Используется функция rand() из awk.
AWKSCRIPT=' { srand(); print rand() } '
# Команды/параметры, передаваемые awk
# Обратите внимание, функция srand() переустанавливает начальное число генератора случайных чисел
echo -n "Случайное число в диапазоне от 0 до 1 = "
echo | awk "$AWKSCRIPT"
exit 0
# Упражнения:
# ---------
# 1) С помощью оператора цикла выведите 10 различных случайных чисел.
# (Подсказка: вам потребуется вызвать функцию "srand()"
# в каждом цикле с разными начальными числами.
# Что произойдет, если этого не сделать?)
# 2) Заставьте сценарий генерировать случайные числа в диапазоне 10 - 100
# используя целочисленный множитель, как коэффициент масштабирования
# 3) То же самое, что и во втором упражнении,
# но на этот раз случайные числа должны быть целыми.

Кроме того, команда date так же может использоваться для генерации псевдослучайных целых чисел. Также при необходимости сгенерировать несколько целых чисел в shell-скрипте можно воспользоваться командой shuf. Следующая команда сгенерирует одно целое число в диапазоне от 100000 до 999999:

shuf -i 100000-999999 -n 1

После ключа -n может быть любое положительное число, задающее количество(!) генерируемых чисел.

Двойные круглые скобки

Эта конструкция во многом похожа на инструкцию let, внутри ((...)) вычисляются арифметические выражения и возвращается их результат. В простейшем случае, конструкция a=$(( 5 + 3 )) присвоит переменной "a" значение выражения "5 + 3", или 8. Но, кроме того, двойные круглые скобки позволяют работать с переменными в стиле языка C.

Пример: Работа с переменными в стиле языка C.

#!/bin/bash
echo
(( a = 23 )) # Присвоение переменной в стиле C, с обоих строн от "=" стоят пробелы.
echo "a (начальное значение) = $a"
(( a++ )) # Пост-инкремент 'a', в стиле C.
echo "a (после a++) = $a"
(( a-- )) # Пост-декремент 'a', в стиле C.
echo "a (после a--) = $a"
(( ++a )) # Пред-инкремент 'a', в стиле C.
echo "a (после ++a) = $a"
(( --a )) # Пред-декремент 'a', в стиле C.
echo "a (после --a) = $a"
echo
(( t = a<45?7:11 )) # Трехместный оператор в стиле языка C.
echo "If a < 45, then t = 7, else t = 11."
echo "t = $t " # Да!
echo# См. так же описание ((...)) в циклах "for" и "while".
# Эта конструкция доступна в Bash, начиная с версии 2.04.
exit 0

Циклы и ветвления

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

Циклы

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

Циклы for
for (in)
Это одна из основных разновидностей циклов. И она значительно отличается от аналога в языке C.

 for arg in [list]
 do 
  команда(ы)...
 done

На каждом проходе цикла, переменная-аргумент цикла 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

Пример: Простой цикл for.

#!/bin/bash
# Список планет.
for planet in Меркурий Венера Земля Марс Юпитер Сатурн Уран Нептун Плутон
do
 echo $planet
done
echo # Если 'список аргументов' заключить в кавычки, то он будет восприниматься как единственный аргумент.
for planet in "Меркурий Венера Земля Марс Юпитер Сатурн Уран Нептун Плутон"
do
 echo $planet
done
exit 0

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

Пример: Цикл 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, можно использовать переменную.

Пример: 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 могут быть использованы имена файлов, которые в свою очередь могут содержать символы-шаблоны.

Пример: Обработка списка файлов в цикле 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 не задан, то в качестве оного используется переменная $@ — список аргументов командной строки.

Пример: Цикл for без списка аргументов.

#!/bin/bash
# Попробуйте вызвать этот сценарий с аргументами и без них и посмотреть на результаты.
for a 
do
 echo -n "$a "
done # Список аргументов не задан, поэтому цикл работает с переменной '$@'
# (список аргументов командной строки, включая пробельные символы).
echo
exit 0

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

Пример: Создание списка аргументов в цикле 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

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

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

#!/bin/bash
# bin-grep.sh: Поиск строк в двоичных файлах.
# замена "grep" для бинарных файлов.
# Аналогично команде "grep -a"
E_BADARGS=65
E_NOFILE=66if [ $# -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

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

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

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

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

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

#!/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 может передаваться другим командам по конвейеру.

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

#!/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 в файл, ниже приводится немного модифицированный вариант предыдущего примера, демонстрирующий эту возможность.

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

#!/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. Для этого используются двойные круглые скобки.

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

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

Пример: Работа с командой 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
Оператор while проверяет условие перед началом каждой итерации и если условие истинно(если код возврата равен 0), то управление передаётся в тело цикла. В отличие от циклов for, циклы while используются в тех случаях, когда количество итераций заранее не известно.

while [condition]
do
 command...
done

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

 while [condition] ; do

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

Пример: Простой цикл 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

Пример: Другой пример цикла 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 может иметь несколько условий. Но только последнее из них определяет возможность продолжения цикла. В этом случае синтаксис оператора цикла должен быть несколько иным.

Пример: Цикл 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-подобной нотации, с использованием двойных круглых скобок.

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

#!/bin/bash
# wh-loopc.sh: Цикл перебора от 1 до 10.
LIMIT=10
a=1
while [ "$a" -le $LIMIT ]
do
 echo -n "$a "
 let "a+=1"
done # Пока ничего особенного.
echo; echo
# +=================================================================+
# А теперь оформим в стиле языка C.
((a = 1)) # a=1
# Двойные скобки допускают наличие лишних пробелов в выражениях.
while (( a <= LIMIT )) # В двойных скобках символ "$" перед переменными опускается.
do
 echo -n "$a "
 ((a += 1)) # let "a+=1"
# Двойные скобки позволяют наращивание переменной в стиле языка C.
done
echo
# Теперь, программисты, пишущие на C, могут чувствовать себя в Bash как дома.
exit 0

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

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

until [condition-is-true]
do 
 command...
done

Обратите внимание: оператор until проверяет условие завершения цикла ПЕРЕД очередной итерацией, а не после, как это принято в некоторых языках программирования. Как и в случае с циклами for/in, при размещении ключевого слова do в одной строке с объявлением цикла, необходимо вставлять символ ";" перед do.

until [condition-is-true] ; do

Пример: Цикл until.

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

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

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

Пример: Вложенный цикл

#!/bin/bash
# Вложенные циклы "for".
outer=1 # Счетчик внешнего цикла.
# Начало внешнего цикла.
for a in 1 2 3 4 5
do
 echo "Итерация #$outer внешнего цикла."
 echo "---------------------"
 inner=1 # Сброс счетчика вложенного цикла.
# Начало вложенного цикла.
 for b in 1 2 3 4 5
 do
  echo "Итерация #$inner вложенного цикла."
  let "inner+=1" # Увеличить счетчик итераций вложенного цикла.
 done
# Конец вложенного цикла.
 let "outer+=1" # Увеличить счетчик итераций внешнего цикла.
 echo # Пустая строка для отделения итераций внешнего цикла.
done
# Конец внешнего цикла.
exit 0

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

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

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

#!/bin/bash
LIMIT=19 # Верхний предел
echo
echo "Печать чисел от 1 до 20 (исключая 3 и 11)."
a=0
while [ $a -le "$LIMIT" ]
do
 a=$(($a+1))
 if [ "$a" -eq 3 ] || [ "$a" -eq 11 ] # Исключить 3 и 11
 then
  continue # Переход в начало цикла.
 fi
 echo -n "$a "
done
# Упражнение:
# Почему число 20 тоже выводится?
echo; echo
echo Печать чисел от 1 до 20, но взгляните, что происходит после вывода числа 2
##################################################################
# Тот же цикл, только 'continue' заменено на 'break'.
a=0
while [ "$a" -le "$LIMIT" ]
do
 a=$(($a+1))
 if [ "$a" -gt 2 ]
 then
  break # Завершение работы цикла.
 fi
 echo -n "$a "
done
echo; echo; echo
exit 0

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

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

#!/bin/bash
# break-levels.sh: Прерывание циклов.
# "break N" прерывает исполнение цикла, стоящего на N уровней выше текущего.
for outerloop in 1 2 3 4 5
do
 echo -n "Группа $outerloop: "
 for innerloop in 1 2 3 4 5
 do
  echo -n "$innerloop "
  if [ "$innerloop" -eq 3 ]
  then
   break # Попробуйте "break 2",
         # тогда будут прерываться как вложенный, так и внешний циклы
  fi
 done
 echo
done
echo
exit 0

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

Пример: Передача управления в начало внешнего цикла.

#!/bin/bash
# Команда "continue N" передаёт управление в начало внешнего цикла, отстоящего от текущего на N уровней.
for outer in I II III IV V # внешний цикл
do
 echo; echo -n "Группа $outer: "
 for inner in 1 2 3 4 5 6 7 8 9 10 # вложенный цикл
 do
  if [ "$inner" -eq 7 ]
  then
   continue 2 # Передача управления в начало цикла 2-го уровня.
              # попробуйте убрать параметр 2 команды "continue"
  fi
  echo -n "$inner " # 8 9 10 никогда не будут напечатаны.
 done
done
echo; echo
# Упражнение:
# Подумайте, где реально можно использовать "continue N" в сценариях.
exit 0

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

# Albert Reiner привёл пример использования "continue N":
# ---------------------------------------------------------
# Допустим, у меня есть большое количество задач, обрабатывающие некоторые данные,
# которые хранятся в некоторых файлах, с именами, задаваемыми по шаблону,
# в заданном каталоге.
# Есть несколько машин, которым открыт доступ к этому каталогу
# и я хочу распределить обработку информации между машинами.
# тогда я обычно для каждой машины пишу нечто подобное:
while true
do
 for n in .iso.*
 do
  [ "$n" = ".iso.opts" ] && continue
  beta=${n#.iso.}
  [ -r .Iso.$beta ] && continue
  [ -r .lock.$beta ] && sleep 10 && continue
  lockfile -r0 .lock.$beta || continue
  echo -n "$beta: " `date`
  run-isotherm $beta
  date
  ls -alF .Iso.$beta
  [ -r .Iso.$beta ] && rm -f .lock.$beta
  continue 2
 done
 break
done
# Конкретная реализация цикла, особенно sleep N, зависит от конкретных применений,
# но в общем случае он строится по такой схеме:
while true
do
 for job in {шаблон}
 do
  {файл уже обработан или обрабатывается} && continue
  {пометить файл как обрабатываемый, обработать, пометить как обработанный}
  continue 2
 done
 break # Или что нибудь подобное `sleep 600', чтобы избежать завершения.
done
# Этот сценарий завершит работу после того как все данные будут обработаны
# (включая данные, которые поступили во время обработки). Использование
# соответствующих lock-файлов позволяет вести обработку на нескольких машинах
# одновременно, не производя дублирующих вычислений [которые, в моем случае,
# выполняются в течении нескольких часов, так что для меня это очень важно].
# Кроме того, поскольку поиск необработанных файлов всегда начинается с
# самого начала, можно задавать приоритеты в именах файлов. Конечно, можно
# обойтись и без `continue 2', но тогда придется ввести дополнительную
# проверку — действительно ли был обработан тот или иной файл
# (чтобы перейти к поиску следующего необработанного файла).

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

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

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

case "$variable" in
"$condition1" )
 command...
;;
"$condition2" )
 command...
;;
esac
  • Заключать переменные в кавычки необязательно, поскольку здесь не производится разбиения на отдельные слова.
  • Каждая строка с условием должна завершаться правой (закрывающей)круглой скобкой — ).
  • Каждый блок команд, отрабатывающих по заданному условию, должен завершаться двумя символами точка-с-запятой — ;;.
  • Блок case должен завершаться ключевым словом esac (case записанное в обратном порядке).

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

#!/bin/bash
echo; echo "Нажмите клавишу и затем клавишу Return."
read Keypress
case "$Keypress" in
 [a-z] ) echo "буква в нижнем регистре";;
 [A-Z] ) echo "Буква в верхнем регистре";;
 [0-9] ) echo "Цифра";;
     * ) echo "Знак пунктуации, пробел или что-то другое";;
esac # Допускается указывать диапазоны символов в [квадратных скобках].
# Упражнение:
# --------
# Сейчас сценарий считывает нажатую клавишу и завершается.
# Измените его так, чтобы сценарий продолжал отвечать на нажатия клавиш,
# но завершался бы только после ввода символа "X".
# Подсказка: заключите всё в цикл "while".
exit 0

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

#!/bin/bash
# Грубый пример базы данных
clear # Очистка экрана
echo "          Список"
echo "          ------"
echo "Выберите интересующую Вас персону:"
echo
echo "[E]vans, Roland"
echo "[J]ones, Mildred"
echo "[S]mith, Julie"
echo "[Z]ane, Morris"
echo
read person
case "$person" in # Обратите внимание: переменная взята в кавычки.
 "E" | "e" )
  # Пользователь может ввести как заглавную, так и строчную букву.
  echo
  echo "Roland Evans"
  echo "4321 Floppy Dr."
  echo "Hardscrabble, CO 80753"
  echo "(303) 734-9874" 
  echo "(303) 734-9892 fax" 
  echo "revans@zzy.net"
  echo "Старый друг и партнер по бизнесу"
  ;;
# Обратите внимание: блок кода, анализирующий конкретный выбор, завершается
# двумя символами "точка-с-запятой".
 "J" | "j" )
  echo
  echo "Mildred Jones"
  echo "249 E. 7th St., Apt. 19" 
  echo "New York, NY 10009" 
  echo "(212) 533-2814" 
  echo "(212) 533-9972 fax" 
  echo "milliej@loisaida.com"
  echo "Подружка"
  echo "День рождения: 11 февраля"
  ;;
# Информация о Smith и Zane будет добавлена позднее.
 * )
# Выбор по-умолчанию.
# "Пустой" ввод тоже обрабатывается здесь.
echo
echo "Нет данных."
  ;;
esac
echo
# Упражнение:
# --------
# Измените этот сценарий таким образом, чтобы он не завершал работу
# после вывода информации о персоне, а переходил на ожидание нового
# ввода от пользователя.
exit 0

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

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

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

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

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

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

#!/bin/bash
# match-string.sh: простое сравнение строк
match_string ()
{
 MATCH=0
 NOMATCH=90
 PARAMS=2 # Функция требует два входных аргумента.
 BAD_PARAMS=91
 [ $# -eq $PARAMS ] || return $BAD_PARAMS
 case "$1" in
 "$2") return $MATCH;;
 * ) return $NOMATCH;;
 esac
}
a=one
b=two
c=three
d=two
match_string $a # неверное число аргументов
echo $? # 91
match_string $a $b # не равны
echo $? # 90
match_string $b $d # равны
echo $? # 0
exit 0

Пример: Проверка ввода.

#!/bin/bash
# isalpha.sh: Использование "case" для анализа строк.
SUCCESS=0
FAILURE=-1
isalpha () # Проверка - является ли первый символ строки символом алфавита.
{
 if [ -z "$1" ] # Вызов функции без входного аргумента?
 then
  return $FAILURE
 fi
 
case "$1" in
 [a-zA-Z] *) return $SUCCESS;; # Первый символ - буква?
 * ) return $FAILURE;;
esac
} # Сравните с функцией "isalpha ()" в языке C.
 
isalpha2 () # Проверка - состоит ли вся строка только из символов алфавита.
{
 [ $# -eq 1 ] || return $FAILURE
 
 case $1 in
 *[!a-zA-Z]*|"") return $FAILURE;;
 *) return $SUCCESS;;
esac
}
isdigit () # Проверка - состоит ли вся строка только из цифр.
{ # Другими словами - является ли строка целым числом.
 [ $# -eq 1 ] || return $FAILURE
 case $1 in
  *[!0-9]*|"") return $FAILURE;;
  *) return $SUCCESS;;
 esac
}
 
check_var () # Интерфейс к isalpha{
{
 if isalpha "$@"
 then
  echo "\"$*\" начинается с алфавитного символа."
  if isalpha2 "$@"
  then # Дальнейшая проверка не имеет смысла, если первй символ не буква.
   echo "\"$*\" содержит только алфавитные символы."
  else
   echo "\"$*\" содержит по меньшей мере один не алфавитный символ."
  fi
 else
  echo "\"$*\" начинается с не алфавитного символа ."
  # Если функция вызвана без входного параметра,
  # то считается, что строка содержит "не алфавитной" символ.
 fi
echo
}
digit_check () # Интерфейс к isdigit ().
{
 if isdigit "$@"
 then
  echo "\"$*\" содержит только цифры [0 - 9]."
 else
  echo "\"$*\" содержит по меньшей мере один не цифровой символ."
 fi
echo
}
a=23skidoo
b=H3llo
c=-What?
d=What?
e=`echo $b` # Подстановка команды.
f=AbcDef
g=27234
h=27a34
i=27.34
check_var $a
check_var $b
check_var $c
check_var $d
check_var $e
check_var $f
check_var # Вызов без параметра, что произойдет?
#
digit_check $g
digit_check $h
digit_check $i
exit 0 # Сценарий дополнен S.C.
# Упражнение:
# --------
# Напишите функцию 'isfloat ()', которая проверяла бы вещественные числа.
# Подсказка: Эта функция подобна функции 'isdigit ()',
# надо лишь добавить анализ наличия десятичной точки.

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

select variable [in list]
do
 command...
 break
done

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

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

#!/bin/bash
PS3='Выберите ваш любимый овощ: ' # строка приглашения к вводу (prompt)
echo
select vegetable in "бобы" "морковь" "картофель" "лук" "брюква"
do
 echo
 echo "Вы предпочитаете $vegetable."
 echo ";-))"
 echo
 break # если 'break' убрать, то получится бесконечный цикл.
done
exit 0

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

for variable [in list]

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

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

#!/bin/bash
PS3='Выберите ваш любимый овощ: '
echo
choice_of()
{
 select vegetable # список выбора [in list] отсутствует, поэтому 'select' использует входные аргументы функции.
 do
  echo
  echo "Вы предпочитаете $vegetable."
  echo ";-))"
  echo
  break
 done
}
choice_of бобы рис морковь редис томат шпинат 
#          $1  $2    $3     $4    $5     $6 
# передача списка выбора в функцию choice_of()
exit 0

Внутренние команды

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

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

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

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

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

Ввод/вывод
echo
Выводит (на stdout) выражение или содержимое переменной.

echo Hello
echo $a

Для вывода экранированных символов, echo требует наличие ключа -e. Обычно, команда echo выводит в конце символ перевода строки. Подавить вывод это символа можно ключом -n.
Команда echo может использоваться для передачи информации по конвейеру другим командам:

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

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

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

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

bash$ ls -l /usr/share/apps/kjezz/sounds
-rw-r--r-- 1 root root 1407 Nov 7 2000 reflect.au
-rw-r--r-- 1 root root 362 Nov 7 2000 seconds.au
bash$ echo `ls -l /usr/share/apps/kjezz/sounds`
total 40 -rw-r--r-- 1 root root 716 Nov 7 2000 reflect.au -rw-r--r-- 1 root root 362 Nov 7 2000 seconds.au

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

bash$ type -a echo
echo is a shell builtin
echo is /bin/echo

printf
printf — команда форматированного вывода, расширенный вариант команды echo и ограниченный вариант библиотечной функции printf() в языке C, к тому же синтаксис их несколько отличается друг от друга.
printf format-string... parameter...
Это встроенная команда Bash. Имеет внешний аналог /bin/printf или /usr/bin/printf. За более подробной информацией обращайтесь к страницам справочного руководства man 1 printf по системным командам.
!Старые версии Bash могут не поддерживать команду printf.

Пример: 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 позволяет записывать значения в массивы.

Пример: Ввод значений переменных с помощью 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.

Пример: Пример использования команды 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 заставляет интерпретировать символ \ как обычный символ.

Пример: Ввод многострочного текста с помощью 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 обнаруживать нажатие курсорных и некоторых других служебных клавиш.

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

#!/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
Команда read может считывать значения для переменных из файла, перенаправленного на stdin. Если файл содержит не одну строку, то переменной будет присвоена только первая строка. Если команде read будет передано несколько переменных, то первая строка файла будет разбита, по пробелам, на несколько подстрок, каждая из которых будет записана в свою переменную. Будьте осторожны!

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

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             # Восстановление предыдущего состояния переменной $
# Эту часть кода написал Heiner Steven.
# Если переменная $IFS устанавливается внутри цикла,
# то отпадает необходимость сохранения её первоначального значения
# во временной переменной.
# Спасибо Dim Segebart за разъяснения.
echo "------------------------------------------------"
echo "Список всех пользователей:"
while IFS=: read name passwd uid gid fullname ignore
do
 echo "$name ($fullname)"
done < /etc/passwd    # перенаправление ввода.
echo
echo "Значение переменной \$IFS осталось прежним: $IFS"
exit 0

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

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

Не смотря на это, как указывает Bjon Eriksson:

Пример: Проблемы чтения данных из канала (конвейера).

#!/bin/sh
# readpipe.sh
# Этот сценарий предоставил Bjon Eriksson.
last="(null)"
cat $0 |
while read line
do
 echo "{$line}"
 last=$line
done
printf "\nКонец, последняя строка:$last\n"
exit 0 # Конец сценария.

Далее следует результат (частично) работы сценария.

./readpipe.sh
{#!/bin/sh}
{last="(null)"}
{cat $0 |}
{while read line}
{do}
{echo "{$line}"}
{last=$line}
{done}
{printf "nКонец, последняя строка:$last\n"}
Конец, последняя строка:(null)
Переменная (last) инициализируется в подоболочке, поэтому она оказывается не инициализированной за его пределами.

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

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

Команда cd с ключом -P\ (physical) игнорирует символические ссылки. Команда "cd -" выполняет переход в каталог $OLDPWD</blue> — предыдущий рабочий каталог. Неожиданным образом выполняется команда cd, если ей передать, в качестве каталога назначения, два слэша.

bash$ cd //
bash$ pwd
//

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

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

pushd,popd,dirs
Этот набор команд является составной частью механизма "закладок" на каталоги и позволяет перемещаться по каталогам вперед и назад в заданном порядке. Для хранения имён каталогов используется стек (LIFO — "последний вошёл, первый вышел").
pushd dir-name — помещает имя текущего каталога в стек и осуществляет переход в каталог dir-name.
popd — выталкивает, находящееся на вершине стека, имя каталога и одновременно осуществляет переход в каталог, оказавшийся на вершине стека.
dirs — выводит содержимое стека каталогов (сравните с переменной $DIRSTACK). В случае успеха, обе команды — pushd и popd автоматически вызывают dirs.
Эти команды могут оказаться весьма полезными, когда в сценарии нужно производить частую смену каталогов, но при этом не хочется жестко "зашивать" имена каталогов. Обратите внимание: содержимое стека каталогов постоянно хранится в переменной-массиве - $DIRSTACK.

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

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

Пример: Команда 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]
Транслирует список аргументов, из списка, в команды.

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

#!/bin/bash
y=`eval ls -l` # Подобно y=`ls -l`
echo $y # но символы перевода строки не выводятся, поскольку имя переменной не в кавычках.
echo
echo "$y" # Если имя переменной записать в кавычках — символы перевода строки сохраняются.
echo; echo
y=`eval df` # Аналогично y=`df`
# Когда производится подавление вывода символов LF (перевод строки), то анализ
# результатов различными утилитами, такими как awk, можно сделать проще.
exit 0

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

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

Пример: Шифрование по алгоритму "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.

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

В программе "test.pl", на языке Perl:
          ...
          my $WEBROOT = <WEBROOT_PATH>;
          ...
Эта попытка подстановки значения переменной вместо её имени:
          $export WEBROOT_PATH=/usr/local/webroot
          $sed 's/<WEBROOT_PATH>/$WEBROOT_PATH/' < test.pl > out 
даст такой результат:
          my $WEBROOT = $WEBROOT_PATH;
Тем не менее:
          $export WEBROOT_PATH=/usr/local/webroot
          $eval sed 's/<WEBROOT_PATH>/$WEBROOT_PATH/' < test.pl > out
# ====
Этот вариант дал желаемый результат — имя переменной, в тексте программы,
благополучно было заменено на её значение:
          my $WEBROOT = /usr/local/webroot

Команда eval может быть небезопасна. Если существует приемлемая альтернатива, то желательно воздерживаться от использования eval. Так, eval $COMMANDS исполняет код, который записан в переменную COMMANDS, которая, в свою очередь, может содержать весьма неприятные сюрпризы, например rm -rf *. Использование команды eval, для исполнения кода неизвестного происхождения, крайне опасно!
set
Команда set изменяет значения внутренних переменных сценария. Она может использоваться для переключения опций (ключей, флагов), определяющих поведение скрипта. Ещё одно применение — сброс/установка позиционных параметров (аргументов), значения которых будут восприняты как результат работы команды (set `command`).

Пример: Установка значений аргументов с помощью команды 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 используется с ключом "--", после которого следует переменная, то значение переменной переносится в позиционные параметры (аргументы). Если имя переменной отсутствует, то эта команда приводит к сбросу позиционных параметров.

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

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

unset
Команда unset удаляет переменную, фактически — устанавливает её значение в null. Обратите внимание: эта команда не может сбрасывать позиционные параметры (аргументы)!

bash$ unset PATH
bash$ echo $PATH
 
bash$

Пример: "Сброс" переменной.

#!/bin/bash
# unset.sh: Сброс переменной.
variable=hello                       # Инициализация.
echo "variable = $variable"
unset variable                       # Сброс.
                                     # Тот же эффект даёт variable=
echo "(unset) variable = $variable"  # $variable = null.
exit 0

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

Пример: Передача переменных во вложенный сценарий 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

Допускается объединение инициализации и экспорта переменной в одну инструкцию: 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. Она позволяет обрабатывать серии опций, объединённых в один аргумент и дополнительные аргументы, передаваемые сценарию (например, scriptname -abc -e/usr/local).
С командой getopts очень тесно взаимосвязаны скрытые переменные. $OPTIND — указатель на аргумент (OPTion INDex) и $OPTARG (OPTion ARGument) — дополнительный аргумент опции. Символ двоеточия, следующий за именем опции, указывает на то, что она имеет дополнительный аргумент.
Обычно getopts упаковывается в цикл while, в каждом проходе цикла извлекается очередная опция и её аргумент (если он имеется), обрабатывается, затем уменьшается на 1 скрытая переменная $OPTIND и выполняется переход к началу новой итерации.

  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))
# Перейти к следующей опции.
# Всё не так сложно, как может показаться ;-)

Пример: Приём опций/аргументов, передаваемых сценарию, с помощью 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". Может найти применение в ситуациях, когда несколько сценариев пользуются одним файлом с данными или библиотекой функций.

Пример: "Подключение" внешнего файла.

#!/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 для пример, представленного выше, должен находиться в том же каталоге.

# Этот файл подключается к сценарию.
# Подключаемые файлы могут содержать об"явления переменных, функций и т.п.
# Загружаться может командой '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
}

Сценарий может подключить даже самого себя, только этому едва ли можно найти какое-либо практическое применение.

Пример: Пример (бесполезный) сценария, который подключает себя самого.

#!/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, чтобы проинформировать родительский процесс об успешном завершении. Если сценарий завершается командой exit без аргументов, то в качестве кода завершения сценария принимается код завершения последней выполненной команды, не считая самой команды exit. exec
Это встроенная команда интерпретатора shell, заменяет текущий процесс новым процессом,запускаемым командой exec. Обычно, когда командный интерпретатор встречает эту команду, то он порождает дочерний процесс, чтобы исполнить команду. При использовании встроенной команды exec, оболочка не порождает ещё один процесс, а заменяет текущий процесс другим. Для сценария это означает его завершение сразу после исполнения команды exec. По этой причине, если вам встретится exec в сценарии, то, скорее всего это будет последняя команда в сценарии.

Пример: Команда exec.

#!/bin/bash
exec echo "Завершение \"$0\"." # Это завершение работы сценария.
# ----------------------------------
# Следующие ниже строки никогда не будут исполнены
echo "Эта строка никогда не будет выведена на экран."
exit 99     # Сценарий завершит работу не здесь.
            # Проверьте код завершения сценария
            # командой 'echo $?'.
            # Он точно не будет равен 99.

Пример: Сценарий, который запускает себя самого.

#!/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. Ключ -exec команды find — это не то же самое, что встроенная команда exec.
shopt
Эта команда позволяет изменять ключи (опции) оболочки на лету. Её часто можно встретить в стартовых файлах, но может использоваться и в обычных сценариях. Требует 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.

Команды управления заданиями

Некоторые из нижеследующих команд принимают, в качестве аргумента, "идентификатор задания" .См. таблицу в конце главы.
jobs
Выводит список заданий, исполняющихся в фоне. Команда ps более информативна. Задания и процессы легко спутать. Некоторые внутренние команды, такие как 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 для предотвращения преждевременного завершения сценария до того, как завершит работу фоновое задание.

Пример: Ожидание завершения процесса перед тем как продолжить работу.

#!/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. См. таблицу идентификации заданий. При запуске команды в фоне из сценария может возникнуть ситуация, когда сценарий приостанавливает свою работу до тех пор, пока не будет нажата клавиша 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
Принудительное завершение процесса путем передачи ему соответствующего сигнала.

Пример: Сценарий, завершающий себя сам с помощью команды kill.

#!/bin/bash
# self-destruct.sh
kill $$ # Сценарий завершает себя сам.
        # Надеюсь вы ещё не забыли, что "$$" — это PID сценария.
echo "Эта строка никогда не будет выведена."
# Вместо него на stdout будет выведено сообщение "Terminated".
exit 0
# Какой код завершения вернет сценарий?
#sh self-destruct.sh
# echo $?
# 143
#
# 143 = 128 + 15
# сигнал TERM

Команда kill -l выведет список всех сигналов. Команда kill -9 — это "жёсткий kill", она используется, как правило, для завершения зависших процессов, которые упорно отказываются "умирать", отвергая простой kill. Иногда достаточно подать команду kill -15. "Процессы-зомби", т.е. процессы, "родители" которых уже завершили работу, не могут быть "убиты" таким способом (невозможно "убить" "мертвого"), рано или поздно с ними"расправится" процесс init.
command
Директива command COMMAND запрещает использование псевдонимов и функций с именем "COMMAND". Это одна из трёх директив командного интерпретатора, которая влияет на обработку команд. Другие две — builtin и enable.
builtin
Конструкция builtin BUILTIN_COMMAND запускает внутреннюю команду "BUILTIN_COMMAND", на время запрещая использование функций и внешних системных команд с тем же именем.
enable
Либо запрещает, либо разрешает вызов внутренних команд. Например, enable -n kill запрещает использование внутренней команды kill, в результате, когда интерпретатор встретит команду kill, то он вызовет внешнюю команду kill, т.е. /bin/kill.
Команда enable -a выведет список всех внутренних команд, указывая для каждой — действительно ли она разрешена. Команда enable -f filename загрузит внутренние команды как разделяемую библиотеку (DLL) из указанного объектного файла. autoload
Перенесена в Bash из ksh. Если функция объявлена как autoload, то она будет загружена из внешнего файла в момент первого вызова. Такой приём помогает экономить системные ресурсы. Обратите внимание: autoload не является частью ядра Bash. Её необходимо загрузить с помощью команды enable -f (см. выше).

Таблица. Идентификация заданий

Нотация Описание
 %N Номер задания [N]
 %S Вызов (командная строка) задания, которая начинается со строки S
 %?S Вызов (командная строка) задания, которая содержит строку S
 %% "текущее" задание (последнее задание приостановленное на переднем плане или запущенное в фоне)
 %+ "текущее" задание (последнее задание приостановленное на переднем плане или запущенное в фоне)
 %- Последнее задание
$! Последний фоновый процесс

Внешние команды, программы и утилиты

Благодаря стандартизации набора команд Unix-систем, сценарии, на языке командной оболочки,могут быть легко перенесены из системы в систему практически без изменений. Мощь сценариев складывается из наборов системных команд и директив командной оболочки с простыми программными конструкциями.

Базовые команды

Первая команда, с которой сталкиваются новички
ls
Команда вывода "списка" файлов. Многие недооценивают всю мощь этой скромной команды. Например, с ключом -R, рекурсивный обход дерева каталогов, команда ls выводит содержимое каталогов в виде древовидной структуры. Вот ещё ряд любопытных ключей (опций) команды ls: -S — сортировка по размеру файлов, -t — сортировка по времени последней модификации файла и -i — выводит список файлов с их inode

Пример: Создание оглавления диска для записи CD-R, с помощью команды ls.

#!/bin/bash
# burn-cd.sh
# Сценарий, автоматизирующий процесс прожигания CD-R.
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)."
 
# "Прожигание" CD-R.
cdrecord -v -isosize speed=$SPEED dev=0,0 $IMAGEFILE
echo "Запись диска."
echo "Наберитесь терпения, это может потребовать некоторого времени."
exit 0

cat,tac
cat — это акроним от concatenate, выводит содержимое списка файлов на stdout. Для объединения файлов в один файл может использоваться в комбинации с операциями перенаправления (> или >>).

# Порядок работы с 'cat'
cat filename                          # Вывод содержимого файла.
cat file.1 file.2 file.3 > file.123   # Объединение содержимого 3-х файлов в одном.

Ключ -n, команды cat, вставляет порядковые номера строк в выходном файле. Ключ -b — нумерует только не пустые строки. Ключ -v выводит непечатаемые символы в нотации с символом ^. Ключ -s заменяет несколько пустых строк, идущих подряд, одной пустой строкой.
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, если он уже существовал.
С флагами -a и -r, или -R выполняет копирование дерева каталогов. mv
Команда перемещения файла. Эквивалентна комбинации команд cp и rm. Может использоваться для перемещения большого количества файлов или для переименования каталогов. При использовании в неинтерактивных сценариях, команде 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 позволяет удалять даже файлы ТОЛЬКО-ДЛЯ-ЧТЕНИЯ и подавляет запрос подтверждения на удаление. При попытке удаления файлов, чьи имена начинаются с символа "-" (дефис), команда rm будет давать сообщение об ошибке.

bash$ rm -badname
rm: invalid option — b
Try `rm --help' for more information.

Как вариант можно предложить предварять имена таких файлов точкой-со-слэшем — "./" (путь к файлу в текущем каталоге).

bash$ rm ./-badname

С ключом -r, удалит все файлы в подкаталогах, начиная с текущего.
rmdir
Удаляет каталог. Удаляемый каталог не должен содержать файлов, включая "скрытые файлы", иначе каталог не будет удалён.
mkdir
Создаёт новый каталог, например:
mkdir -p project/programs/December
создаёт каталог с заданным именем в требуемом каталоге. Ключ -p позволяет создавать промежуточные родительские каталоги.
chmod
Изменяет атрибуты существующего файла.

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
Создаёт ссылку на существующий файл. Позволяет задавать несколько имён одному и тому же файлу и представляет из себя превосходную альтернативу "псевдонимам" (алиасам). Размер файла-ссылки, создаваемого командой ln, имеет длину всего в несколько байт.
Чаще всего команда ln используется с ключом -s, который служит для создания символической(symbolic), или "мягкой" ("soft") ссылки. Без этого флага, команда создает полноценную копию имени файла. С ключом -- только ссылку, указывающую на заданный файл. Дополнительное преимущество ключа -s состоит в том, что он позволяет создавать ссылки на файлы, расположенные в других файловых системах.
Синтаксис команды достаточно прост. Например, команда:
ln -s oldfile newfile создаст ссылку, с именем newfile, на существующий файл oldfile, если файл с именем newfile уже существует, то он будет удален командой ln.

Какого типа ссылки лучше использовать?
John Macdonald дает свой ответ на этот вопрос:

Без сомнения, оба варианта ссылок предоставляют возможность обращаться к файлу (к его содержимому). Так, если вы редактируете содержимое файла, загрузив его в редактор по имени ссылки, то внесённые изменения вы увидите, если откроете файл с использованием его оригинального имени. Различия между "жёсткими" и "мягкими" ссылками начинают проявляться когда вы поднимаетесь на более высокий уровень. Удаление или переименование файла, на который имеется "жёсткая" ссылка, никак не воздействует на ссылку. Она по прежнему продолжает ссылаться на файл (файл физически удаляется с диска только тогда, когда будет удалена последняя "жёсткая" ссылка на него, прим. перев.). В случае же с "мягкой" ссылкой всё наоборот — файл удаляется (переименовывается), а ссылка продолжает ссылаться на прежнее имя файла (так получаются "битые" ссылки, прим. перев.). Но главное преимущество "мягких" ссылок перед "жёсткими" состоит в том, что они могут указывать на файлы в других файловых системах (фактически, "мягкие" ссылки ссылаются не на дисковые блоки, где расположен файл, а на имя этого файла).

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

Пример: Здравствуй или Прощай.

#!/bin/bash
# hello.sh: Говорит "hello" или "goodbye"
# в зависимости от того под каким именем был вызван сценарий.
# Создайте ссылку на этот сценарий:
# ln -s hello.sh goodbye
# И попробуйте вызвать его под разными именами:
# ./hello.sh
# ./goodbye
HELLO_CALL=65
GOODBYE_CALL=66
if [ $0 = "./goodbye" ]
then
 echo "Пока!"
 # Можете вставить свой вариант прощания.
 exit $GOODBYECALL
fi
echo "Привет!"
# Можете вставить свой вариант приветствия.
exit $HELLO_CALL

man,info
Команды доступа к справочным и информационным страницам по системным командам и установленным программам и утилитам. Как правило, страницы info содержат более подробную информацию, чем man.

Более сложные команды

Команды для более опытных пользователей
find
-exec COMMAND \;
Для каждого найденного файла, соответствующего заданному шаблону поиска, выполняет команду COMMAND. Командная строка должна завершаться последовательностью символов \; (здесь символ ";" экранирован обратным слэшем, чтобы информировать командную оболочку о том, что символ ";" должен быть передан команде find как обычный символ). Если COMMAND содержит {}, то find подставляет полное имя найденного файла вместо "{}".

bash$ find ~/ -name '*.txt'
/home/bozo/.kde/share/apps/karm/karmdata.txt
/home/bozo/misc/irmeyc.txt
/home/bozo/test-scripts/1.txt

find /home/bozo/projects -mtime 1# Найти все файлы в каталоге /home/bozo/projects и вложенных подкаталогах,
# Найти все файлы в каталоге /home/bozo/projects и вложенных подкаталогах,
# которые изменялись в течение последних суток.
# mtime = время последнего изменения файла
# ctime = время последнего изменения атрибутов файла (через 'chmod' или как-то иначе)
# atime = время последнего обращения к файлу
DIR=/home/bozo/junk_files
find "$DIR" -type f -atime +5 -exec rm {} \;
# Удалить все файлы в каталоге "/home/bozo/junk_files"
# к которым не было обращений в течение последних 5 дней.
# "-type filetype", где
# f = обычный файл
# d = каталог, и т.п.
# (Полный список ключей вы найдете в 'man find'.)
find /etc -exec grep '[0-9][0-9]*[.][0-9][0-9]*[.][0-9][0-9]*[.][0-9][0-9]*' {} \;
# Поиск всех IP-адресов (xxx.xxx.xxx.xxx) в файлах каталога /etc.
# Однако эта команда выводит не только IP-адреса, как этого избежать?
# Примерно так:
find /etc -type f -exec cat '{}' \; | tr -c '.[:digit:]' '\n' \
| grep '^[^.][^.]*\.[^.][^.]*\.[^.][^.]*\.[^.][^.]*$'
# [:digit:] — один из символьных классов
# введен в стандарт POSIX 1003.2.
# Спасибо S.C.

Не следует путать опцию -exec команды find с внутренней командой Bash — exec.

Пример: Badname, удаление файлов в текущем каталоге, имена которых содержат недопустимые символы и пробелы.

#!/bin/bash
# Удаление файлов в текущем каталоге, чьи имена содержат недопустимые символы.
for filename in *
do
 badname=`echo "$filename" | sed -n /[\+\{\;\"\\\=\?~\(\)\<\>\&\*\|\$]/p`
 # Недопустимые символы в именах файлов: + { ; " \ = ? ~ ( ) < > & * | $
 rm $badname 2>/dev/null # Сообщения об ошибках "выстреливаются" в никуда.
done
# Теперь "позаботимся" о файлах, чьи имена содержат пробельные символы.
find . -name "* *" -exec rm -f {} \;
# На место "{}", find подставит полное имя файла.
# Символ '\' указывает на то, что ';' интерпретируется как обычный символ, а не как конец команды.
exit 0
#---------------------------------------------------------------------
# Строки, приведённые ниже, не будут выполнены, т.к. выше стоит команда "exit".
# Альтернативный вариант сценария:
find . -name '*[+{;"\\=?~()<>&*|$ ]*' -exec rm -f '{}' \;
exit 0
# (Спасибо S.C.)

Пример: Удаление файла по его номеру inode.

#!/bin/bash
# idelete.sh: Удаление файла по номеру inode.
# Этот прием используется в тех случаях, когда имя файла начинается с недопустимого символа,
# например, ? или -.
ARGCOUNT=1                   # Имя файла должно быть передано в сценарий.
E_WRONGARGS=70
E_FILE_NOT_EXIST=71
E_CHANGED_MIND=72
if [ $# -ne "$ARGCOUNT" ]
then
 echo "Порядок использования: `basename $0` filename"
 exit $E_WRONGARGS
fi
if [ ! -e "$1" ]
then
 echo "Файл \""$1"\" не найден."
 exit $E_FILE_NOT_EXIST
fi
inum=`ls -i | grep "$1" | awk '{print $1}'`
# inum = номер inode (index node) файла
# Каждый файл имеет свой inode, где хранится информация о физическом расположении файла.
echo; echo -n "Вы совершенно уверены в том, что желаете удалить \"$1\" (y/n)? "
# Ключ '-v' в команде 'rm' тоже заставит команду вывести подобный запрос.
read answer
case "$answer" in
 [nN]) echo "Передумали?"
       exit $E_CHANGED_MIND
       ;;
 *) echo "Удаление файла \"$1\".";;
esac
find . -inum $inum -exec rm {} \;
echo "Файл "\"$1"\" удалён!"
exit 0

Дополнительные примеры по использованию команды find вы найдете в страницах справочного руководства (man find) вы найдете более подробную информацию об этой достаточно сложной и мощной команде. xargs
Команда передачи аргументов указанной команде. Она разбивает поток аргументов на отдельные составляющие и поочередно передаёт их заданной команде для обработки. Эта команда может рассматриваться как мощная замена обратным одинарным кавычкам. Зачастую, когда команды, заключенные в обратные одиночные кавычки, завершаются с ошибкой too many arguments (слишком много аргументов), использование xargs позволяет обойти это ограничение. Обычно, xargs считывает список аргументов со стандартного устройства ввода stdin или из канала (конвейера), но может считывать информацию и из файла.
Если команда не задана, то по-умолчанию выполняется echo. При передаче аргументов по конвейеру, xargs допускает наличие пробельных символов и символов перевода строки, которые затем автоматически отбрасываются.

bash$ ls -l
total 0
-rw-rw-r-- 1 bozo bozo 0 Jan 29 23:58 file1
-rw-rw-r-- 1 bozo bozo 0 Jan 29 23:58 file2
bash$ ls -l | xargs
total 0 -rw-rw-r-- 1 bozo bozo 0 Jan 29 23:58 file1 -rw-rw-r-- 1 bozo bozo 0 Jan 29 23:58
file2

ls | xargs -p -l gzip
— упакует с помощью gzip все файлы в текущем каталоге, выводя запрос на подтверждение для каждого файла.

xargs имеет очень любопытный ключ -n NN, который ограничивает количество передаваемых аргументов за один "присест" числом NN.

ls | xargs -n 8 echo
— выведет список файлов текущего каталога в 8 колонок.

Ещё одна полезная опция — -0, в комбинации с find -print 0 или grep -lZ позволяет обрабатывать аргументы, содержащие пробелы и кавычки.

find / -type f -print0 | xargs -0 grep -liwZ GUI | xargs -0 rm -f

grep -rliwZ GUI / | xargs -0 rm -f

Обе вышеприведенные команды удалят все файлы, содержащие в своём имени комбинацию символов "GUI". (Спасибо S.C.)

Пример: Использование команды xargs для мониторинга системного журнала.

#!/bin/bash
# Создание временного файла мониторинга в текущем каталоге,
# куда переписываются несколько последних строк из /var/log/messages.
# Обратите внимание: если сценарий запускается обычным пользователем,
# то файл /var/log/messages должен быть доступен на чтение этому пользователю.
# #root chmod 644 /var/log/messages
LINES=5( date; uname -a ) >>logfile
# Время и информация о системе
echo --------------------------------------------------------------------- >>logfile
tail -$LINES /var/log/messages | xargs | fmt -s >>logfile
echo >>logfile
echo >>logfileexit 0
# Упражнение:
# --------
# Измените сценарий таким образом, чтобы он мог отслеживать изменения в /var/log/messages
# с интервалом в 20 минут.
# Подсказка: воспользуйтесь командой "watch".

Пример: copydir, копирование файлов из текущего каталога в другое место.

#!/bin/bash
# Копирует все файлы из текущего каталога
# в каталог, указанный в командной строке.
if [ -z "$1" ] # Выход, если каталог назначения не задан.
then
 echo "Порядок использования: `basename $0` directory-to-copy-to"
exit 65
fi
ls . | xargs -i -t cp ./{} $1 # Этот сценария является точным эквивалентом
# cp * $1
# если в именах файлов не содержатся пробельные символы.
exit 0

Пример: Завершение работы процесса по его имени.

#!/bin/bash
# kill-byname.sh: Завершение работы процесса по его имени.
# Сравните этот сценарий с kill-process.sh.
# Пример,
# Попробуйте запустить команду "./kill-byname.sh xterm" —
#+ и понаблюдайте как закроются все окна xterm.
# Внимание:
# --------
# Этот сценарий может представлять определенную угрозу.
# Запуск этого сценария (особенно с правами root)
# может привести к потере несохранённых данных и другим неожиданным эффектам.
E_BADARGS=66
if test -z "$1" # Проверка — задано ли имя процесса 
then
 echo "Порядок использования: `basename $0` останавливаемый(ые)_процесс(ы)"
 exit $E_BADARGS
fi
# ---------------------------------------------------------
# Примечание:
# Ключ -i команды xargs — это "замена строки" .
# Фигурные скобки — это замещаемая строка.
# 2&>/dev/null — подавляет вывод сообщений об ошибках.
# ---------------------------------------------------------
PROCESS_NAME="$1"
ps ax | grep "$PROCESS_NAME" | awk '{print $1}' | xargs -i kill {} 2&>/dev/null
exit $?

Пример: Подсчёт частоты встречаемости слов using xargs.

#!/bin/bash
# wf2.sh: Грубый подсчёт частоты встречаемости слова в текстовом файле.
# Команда 'xargs' используется для выделения отдельных слов из строки.
# Сравните этот сценарий с "wf.sh".
# Проверка — задано ли имя файла из командной строки.
ARGS=1
E_BADARGS=65
E_NOFILE=66
if [ $# -ne "$ARGS" ]
# Передано корректное число аргументов?
then
 echo "Порядок использования: `basename $0` имя_файла"
 exit $E_BADARGS
fi
if [ ! -f "$1" ] # Проверка наличия файла.
then
 echo "Файл \"$1\" не найден."
 exit $E_NOFILE
fi
#######################################################
cat "$1" | xargs -n1 | \
# Вывод содержимого файла, по одному слову в строке.
tr A-Z a-z | \
# Преобразование в нижний регистр.
sed -e 's/\.//g' -e 's/\,//g' -e 's/ /\
/g' | \
# Отбросить точки и запятые и
# заменить пробелы символами перевода строки,
sort | uniq -c | sort -nr # В заключение — подсчитать и отсортировать по частоте встречаемости.
#######################################################
# Этот сценарий выполняет те же действия, что и "wf.sh",
# но он более "тяжелый" и работает значительно медленнее.
exit 0

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

expr 3 + 5

возвратит 8

expr 5 % 3

возвратит 2

expr 5 \* 3

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

y=`expr $y + 1`

Операция инкремента переменной, то же самое, что и let y=y+1, или y=$(($y+1)). Пример подстановки арифметических выражений.

z=`expr substr $string $position $length`

Извлекает подстроку длиной $length символов, начиная с позиции $position.

Пример: Пример работы с expr.

#!/bin/bash
# Демонстрация некоторых приемов работы с командой 'expr'
# =======================================
echo
# Арифметические операции
# ----------------------
echo "Арифметические операции"
echo 
a=`expr 5 + 3`
echo "5 + 3 = $a"
a=`expr $a + 1`
echo
echo "a + 1 = $a"
echo "(инкремент переменной)"
 
a=`expr 5 % 3`
# остаток от деления (деление по модулю)
echo
echo "5 mod 3 = $a"
echo
echo# Логические операции
# ------------------
 
# Возвращает 1 если выражение истинно, 0 — если ложно,
# в противоположность соглашениям, принятым в Bash.
 
echo "Логические операции"
echo
x=24
y=25
b=`expr $x = $y` # Сравнение.
echo "b = $b" # 0 ( $x -ne $y )
echo
a=3b=`expr $a \> 10`
echo 'b=`expr $a \> 10`, поэтому...'
echo "Если a > 10, то b = 0 (ложь)"
echo "b = $b" # 0 ( 3 ! -gt 10 )
echo
b=`expr $a \< 10`
echo "Если a < 10, то b = 1 (истина)"
echo "b = $b" # 1 ( 3 -lt 10 )
echo
# Обратите внимание на необходимость экранирования операторов.
b=`expr $a \<= 3`
echo "Если a <= 3, то b = 1 (истина)"
echo "b = $b" # 1 ( 3 -le 3 )
# Существует еще оператор "\>=" (больше или равно).
echo
echo
# Операции сравнения
#-----------------
echo "Операции сравнения"
echo
a=zipper
echo "a is $a"
if [ `expr $a = snap` ]
then
 echo "a -- это не zipper"
fi
echo
echo
 
# Операции со строками
#------------------
echo "Операции со строками"
echo
a=1234
zipper43231
echo "Строка над которой производятся операции: \"$a\"."
# length: длина строки
b=`expr length $a`
echo "длина строки \"$a\" равна $b."
# index: позиция первого символа подстроки в строке
b=`expr index $a 23`
echo "Позиция первого символа \"2\" в строке \"$a\" : \"$b\"."
# substr: извлечение подстроки, начиная с заданной позиции, указанной длины
b=`expr substr $a 2 6`
echo "Подстрока в строке \"$a\", начиная с позиции 2,\
и длиной в 6 символов: \"$b\"."
# При выполнении поиска по шаблону, по-умолчанию поиск
# начинается с ***начала*** строки.
#
# Использование регулярных выражений
b=`expr match "$a" '[0-9]*'` # Подсчёт количества цифр.
echo Количество цифр с начала строки \"$a\" : $b.
b=`expr match "$a" '\([0-9]*\)'` # Обратите внимание на экранирование круглых скобок
# == ==
echo "Цифры, стоящие в начале строки \"$a\" : \"$b\"."
echo
exit 0

Вместо оператора match можно использовать оператор : . Например, команда

b=`expr $a : [0-9]*`

является точным эквивалентом для

b=`expr match $a [0-9]*`
в примере, рассмотренном выше.
#!/bin/bash
echo
echo "Операции над строками с использованием конструкции \"expr \$string : \""
echo
"========================================================================"
echo
a=1234zipper5FLIPPER43231
 
echo "Строка, над которой выполняются операции: \"`expr "$a" : '\(.*\)'`\"."
# Экранирование круглых скобок в шаблоне                         == ==
 
# Если скобки не экранировать...
# то 'expr' преобразует строковый операнд в целое число.
echo "Длина строки \"$a\" равна `expr "$a" : '.*'`." 
# Длина строки
echo "Количество цифр с начала строки \"$a\" равно `expr "$a" : '[0-9]*'`."
# -------------------------------------------------------------------------
#
echo
echo "Цифры, стоящие в начале строки \"$a\" : `expr "$a" : '\([0-9]*\)'`."
#                                                              ==      ==
echo "Первые 7 символов в строке \"$a\" : `expr "$a" : '\(.......\)'`."
#     ======                                           ==       ==
# Опять же, необходимо экранировать круглые скобки в шаблоне.
#
echo "Последние 7 символов в строке \"$a\" : `expr "$a" : '.*\(.......\)'`."
#     =========                      оператор конца строки ^^
# (фактически означает переход через любое количество символов, пока
# не будет найдена требуемая подстрока)
echo
exit 0

Этот пример демонстрирует необходимость экранирования оператора группировки — \( ... \) в регулярных выражениях, при поиске по шаблону командой expr. Perl, sed и awk имеют в своем распоряжении более мощный аппарат анализа строк. Коротенький скрипт на sed или awk, внутри сценария — значительно более привлекательная альтернатива использованию expr при анализе строк.

Команды для работы с датой и временем

Время/дата и измерение интервалов времени
date
Команда date без параметров выводит дату и время на стандартное устройство вывода stdout. Она становится гораздо интереснее при использовании дополнительных ключей форматирования вывода.

Пример: Команда date.

#!/bin/bash
# Примеры использования команды 'date'echo "Количество дней, прошедших с начала года: `date +%j`."
# Символ '+' обязателен при использовании форматирующего аргумента
# %j, возвращающего количество дней, прошедших с начала года.
echo "Количество секунд, прошедших с 01/01/1970 : `date +%s`."
# %s количество секунд, прошедших с начала "эпохи UNIX",
# но насколько этот ключ полезен?
 
prefix=tempsuffix=`eval date +%s` 
# Ключ "+%s" характерен для GNU-версии 'date'.
filename=$prefix.$suffix
echo $filename
# Прекрасный способ получения "уникального" имени для временного файла,
# даже лучше, чем с использованием $$.
# Дополнительную информацию вы найдете в 'man date'.
exit 0

Ключ -u даёт UTC время (Universal Coordinated Time — время по Гринвичу).

bash$ date
Fri Mar 29 21:07:39 MST 2002
bash$ date -u
Sat Mar 30 04:07:42 UTC 2002

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

date +%N | sed -e 's/000$//' -e 's/^0//'
#          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# Удаление ведущих и завершающих нулей, если таковые присутствуют.

zdump
Отображает время для указанной временной зоны.

bash$ zdump EST
EST Tue Sep 18 22:09:22 2001 EST

time
Выводит подробную статистику по исполнению некоторой команды.

time ls -l /

даст нечто подобное:
0.00 user 0.01 system 0:00.05 elapsed 16% CPU (0 avgtext + 0a vgdata 0 maxresident) k 0 inputs + 0 outputs (149 major + 27 minor) page faults 0 swaps
Начиная с версии 2.0 Bash, команда time стала зарезервированным словом интерпретатора, с несколько изменённым поведением в конвейере.
touch
Утилита устанавливает время последнего обращения/изменения файла в текущее системное время или в заданное время, но так же может использоваться для создания нового пустого файла. Команда touch zzz создаст новый пустой файл с именем zzz, если перед этим файл zzz отсутствовал. Кроме того, такие пустые файлы могут использоваться для индикации, например, времени последнего изменения в проекте.
Эквивалентом команды touch могут служить : >> newfile или >> newfile (для обычных файлов).
at
Команда at — используется для запуска заданий в заданное время. В общих чертах она напоминает cron, однако, at используется для однократного запуска набора команд. at 2pm January 15 — попросит ввести набор команд, которые необходимо запустить в указанное время. Эти команды должны быть совместимыми со сценариями командной оболочки. Ввод завершается нажатием комбинации клавиш Ctl-D.
Ключ -f или операция перенаправления ввода (<), заставляет at прочитать список команд из файла. Этот файл должен представлять из себя обычный сценарий, на языке командной оболочки и, само собой разумеется, такой сценарий должен быть неинтерактивным. Может использоваться совместно с командой run-parts для запуска различных наборов сценариев.

bash$ at 2:30 am Friday < at-jobs.listjob 2 at 2000-10-27 02:30

batch
Команда batch, управляющая запуском заданий, напоминает команду at, но запускает список команд только тогда, когда загруженность системы упадет ниже .8. Подобно команде at, с ключом -f, может считывать набор команд из файла.
cal
Выводит на stdout аккуратно отформатированный календарь на текущий месяц. Может выводить календарь за определенный год.
sleep
Приостанавливает исполнение сценария на заданное количество секунд, ничего не делая. Может использоваться для синхронизации процессов, запущенных в фоне, проверяя наступление ожидаемого события так часто, как это необходимо.

sleep 3      # Пауза, длительностью в 3 секунды.

Команда sleep по-умолчанию принимает количество секунд, но ей можно передать и количество часов и минут и даже дней.

sleep 3 h          # Приостановка на 3 часа!

Для запуска команд через заданные интервалы времени лучше использовать watch.
usleep
Microsleep (здесь символ "u" должен читаться как буква греческого алфавита — "мю", или префикс микро). Это то же самое, что и sleep, только интервал времени задается в микросекундах. Может использоваться для очень тонкой синхронизации процессов.

usleep 30         # Приостановка на 30 микросекунд.

Эта команда является частью пакета initscripts/rc-scripts в дистрибутиве Red Hat. Команда usleep не обеспечивает особую точность соблюдения интервалов, и поэтому она не подходит для применений, критичных ко времени.
hwclock,clock
Команда hwclock используется для получения доступа или коррекции аппаратных часов компьютера. С некоторыми ключами требует наличия привилегий root. Сценарий /etc/rc.d/rc.sysinit использует команду hwclock для установки системного времени во время загрузки. Команда clock — это синоним команды hwclock.

Команды обработки текста

sort
Сортирует содержимое файла, часто используется как промежуточный фильтр в конвейерах. Эта команда сортирует поток текста в порядке убывания или возрастания, в зависимости от заданных опций. Ключ -m используется для сортировки и объединения входных файлов. В странице info перечислено большое количество возможных вариантов ключей.
tsort
Топологическая сортировка, считывает пары строк, разделённых пробельными символами, и выполняет сортировку, в зависимости от заданного шаблона.
uniq
Удаляет повторяющиеся строки из отсортированного файла. Эту команду часто можно встретить в конвейере с командой sort.

cat list-1 list-2 list-3 | sort | uniq > final.list
# Содержимое файлов, сортируется,
# затем удаляются повторяющиеся строки,
# и результат записывается в выходной файл final.list.

Ключ -c выводит количество повторяющихся строк.

bash$ cat testfile
Эта строка встречается только один раз.
Эта строка встречается дважды.
Эта строка встречается дважды.
Эта строка встречается трижды.
Эта строка встречается трижды.
Эта строка встречается трижды.
 
bash$ uniq -c testfile
1 Эта строка встречается только один раз.
2 Эта строка встречается дважды.
3 Эта строка встречается трижды.
 
bash$ sort testfile | uniq -c | sort -nr
3 Эта строка встречается трижды.
2 Эта строка встречается дважды.
1 Эта строка встречается только один раз.

Команда sort INPUTFILE | uniq -c | sort -nr выводит статистику встречаемости строк в файле INPUTFILE (ключ -nr, в команде sort, означает сортировку в порядке убывания). Этот шаблон может с успехом использоваться при анализе файлов системного журнала, словарей и везде, где необходимо проанализировать лексическую структуру документа.

Пример: Частота встречаемости отдельных слов.

#!/bin/bash
# wf.sh: "Сырой" анализ частоты встречаемости слова в текстовом файле.
ARGS=1
E_BADARGS=65
E_NOFILE=66
if [ $# -ne "$ARGS" ] # Файл для анализа задан?
then
 echo "Порядок использования: `basename $0` filename"
 exit $E_BADARGS
fi
if [ ! -f "$1" ] # Проверка существования файла.
then
 echo "Файл \"$1\" не найден."
 exit $E_NOFILE
fi
########################################################
# main ()
sed -e 's/\.//g' -e 's/ /\
/g' "$1" | tr 'A-Z' 'a-z' | sort | uniq -c | sort -nr
#                           =========================
#                           Подсчёт количества вхождений
# Точки и пробелы заменяются
# символами перевода строки,
# затем символы переводятся в нижний регистр
# и наконец подсчитывается количество вхождений,
# и выполняется сортировка по числу вхождений.
########################################################
# Упражнения:
# ---------
# 1) Добавьте команду 'sed' для отсечения других знаков пунктуации, например, запятых.
# 2) Добавьте удаление лишних пробелов и других пробельных символов.
# 3) Добавьте дополнительную сортировку так, чтобы слова с одинаковой частотой встречаемости
# сортировались бы в алфавитном порядке.
exit 0

bash$ cat testfile
Эта строка встречается только один раз.
Эта строка встречается дважды.
Эта строка встречается дважды.
Эта строка встречается трижды.
Эта строка встречается трижды.
Эта строка встречается трижды.
 
bash$ ./wf.sh testfile
6 Эта
6 встречается
6 строка
3 трижды
2 дважды
1 только
1 один
1 раз

expand,unexpand
Команда expand преобразует символы табуляции в пробелы. Часто используется в конвейерной обработке текста.
Команда unexpand преобразует пробелы в символы табуляции. Т.е. она является обратной по отношению к команде expand. cut
Предназначена для извлечения отдельных полей из текстовых файлов. Напоминает команду print $N в awk, но более ограничена в своих возможностях. В простейших случаях может быть неплохой заменой awk в сценариях. Особую значимость, для команды cut, представляют ключи -d (разделитель полей) и -f (номер(а) поля(ей)).
Использование команды cut для получения списка смонтированных файловых систем:

cat /etc/mtab | cut -d ' ' -f1,2

Использование команды cut для получения версии ОС и ядра:

uname -a | cut -d" " -f1,3,11,12

Использование команды cut для извлечения заголовков сообщений из электронных писем:

bash$ grep '^Subject:' read-messages | cut -c10-80
Re: Linux suitable for mission-critical apps?
MAKE MILLIONS WORKING AT HOME3
Spam complaint
Re: Spam complaint

Использование команды cut при разборе текстового файла:

# Список пользователей в /etc/passwd.
FILENAME=/etc/passwd
for user in $(cut -d: -f1 $FILENAME)
do
 echo $user
done# Спасибо Oleg Philon за этот пример.

cut -d ' ' -f2,3 filename эквивалентно awk -F'[ ]' '{ print $2, $3 }' filename
paste
Используется для объединения нескольких файлов в один многоколоночный файл.
join
Может рассматриваться как команда, родственная команде paste. Эта мощная утилита позволяет объединять два файла по общему полю, что представляет собой упрощённую версию реляционной базы данных.
Команда join оперирует только двумя файлами и объединяет только те строки, которые имеют общее поле (обычно числовое), результат объединения выводится на stdout. Объединяемые файлы должны быть отсортированы по ключевому полю.

File: 1.data
100 Shoes
200 Laces
300 Socks
 
File: 2.data
100 $40.00
200 $1.00
300 $2.00
 
bash$ join 1.data 2.data
File: 1.data 2.data
100 Shoes $40.00
200 Laces $1.00
300 Socks $2.00

На выходе ключевое поле встречается только один раз.
head
Выводит начальные строки из файла на stdout (по-умолчанию — 10 строк, но это число можно задать иным). Эта команда имеет ряд интересных ключей.

Пример: Какие из файлов являются сценариями?

#!/bin/bash
# script-detector.sh: Отыскивает файлы сценариев в каталоге.
TESTCHARS=2 # Проверяются первые два символа.
SHABANG='#!' # Сценарии как правило начинаются с "sha-bang."
for file in * # Обход всех файлов в каталоге.
do
 if [[ `head -c$TESTCHARS "$file"` = "$SHABANG" ]]
 # head -c2 #!
 # Ключ '-c' в команде "head" выводит заданное
 # количество символов, а не строк.
 then
  echo "Файл \"$file\" -- сценарий."
 else
  echo "Файл \"$file\" не является сценарием."
 fi
done
exit 0

Пример: Генератор 10-значных случайных чисел.

#!/bin/bash
# rnd.sh: Генератор 10-значных случайных чисел
# Автор: Stephane Chazelas.
head -c4 /dev/urandom | od -N4 -tu4 | sed -ne '1s/.* //p'
# =================================================================== #
# Описание
# --------
# head:
# -c4 — первые 4 байта.
# od:
# -N4 ограничивает вывод 4-мя байтами.
# -tu4 беззнаковый десятичный формат вывода.
# sed:
# -n, в комбинации с флагом "p", в команде "s",
# выводит только совпадающие с шаблоном строки.
 
# Автор сценария описывает действия 'sed' таким образом:
# head -c4 /dev/urandom | od -N4 -tu4 | sed -ne '1s/.* //p'
# ----------------------------------> |
# Передаёт вывод в "sed"    --------> |
# пусть это будет 0000000 1198195154\n
# sed начинает читать символы: 0000000 1198195154\n.
# Здесь он находит символ перевода строки,
# таким образом он получает строку (0000000 1198195154).
# Затем он просматривает <диапазон><действие>. Первый и единственный — это
# диапазон действие# 1 s/.* //p
# Номер строки попадает в заданный диапазон, так что теперь он приступает к выполнению действия:
# пытается заменить наибольшую подстроку, заканчивающуюся пробелом
# ("0000000 ") "ничем" (//), и если замена произведена — выводит результат
# ("p" -- это флаг команды "s", а не команда "p", которая имеет иное значение).
# теперь sed готов продолжить чтение входного потока. (Обратите внимание:
# если опустить ключ -n, то sed выведет строку ещё раз)
# Теперь sed дочитывает остаток строки.
# Он готов приступить к анализу 2-й строки (которая отмечена '$'
# как последняя).
# Поскольку строка не попадает в заданный <диапазон>, на этом обработка прекращается.
# Проще говоря, команда sed означает:
# "В первой строке удалить любые символы, вплоть до последнего встреченного пробела, и затем вывести остаток."
# Сделать это можно более простым способом:
#                        sed -e 's/.* //;q'
# Где, заданы два <диапазона><действия> (можно записать и по другому
# sed -e 's/.* //' -e q):
# диапазон                       действие
# ничего (для совпадающих строк) s/.* //
# ничего (для совпадающих строк) q (quit)
# Здесь sed считывает только первую строку.
# Выполняет оба действия, и выводит строку перед завершением
# (действие "q"), поскольку ключ "-n" опущен.
# =================================================================== #
# Простая альтернатива:
#                        head -c4 /dev/urandom| od -An -tu4
exit 0

tail
Выводит последние строки из файла на stdout (по-умолчанию — 10 строк). Обычно используется для мониторинга системных журналов. Ключ -f, позволяет вести непрерывное наблюдение за добавляемыми строками в файл.

Пример: Мониторинг системного журнала с помощью tail.

#!/bin/bash
filename=sys.log
cat /dev/null > $filename; echo "Создание / очистка временного файла."
# Если файл отсутствует, то он создаётся,
# и очищается, если существует.
# : > filename и > filename дают тот же эффект.
tail /var/log/messages > $filename
# Файл /var/log/messages должен быть доступен для чтения.
echo "В файл $filename записаны последние строки из /var/log/messages."
exit 0

grep
Многоцелевая поисковая утилита, использующая регулярные выражения. Изначально это была команда в древнем строчном редакторе ed, g/re/p, что означает — global-regular expression-print.

grep pattern [file...]

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

bash$ grep '[rst]ystem.$' osinfo.txt
The GPL governs the distribution of the Linux operating system.

Если файл(ы) для поиска не задан, то команда grep работает как фильтр для устройства stdout, например в конвейере.

bash$ ps ax | grep clock
765 tty1 S 0:00 xclock
901 pts/1 S 0:00 grep clock

-i — выполняется поиск без учёта регистра символов.
-w — поиск совпадений целого слова.
-l — вывод только имён файлов, в которых найдены участки, совпадающие с заданным образцом/шаблоном, без вывода совпадающих строк.
-r — (рекурсивный поиск) поиск выполняется в текущем каталоге и всех вложенных подкаталогах.
-n — отображает соответствующие строки вместе с номерами строк.
bash$ grep -n Linux osinfo.txt
2:This is a file containing information about Linux.
6:The GPL governs the distribution of the Linux operating system.
-v (или --invert-match) — выводит только строки, не содержащие совпадений.
grep pattern1 *.txt | grep -v pattern2
# Выводятся строки из "*.txt", совпадающие с "pattern1",
# но ***не*** совпадающие с "pattern2".
-c (--count) — выводит количество совпадений без вывода самих совпадений.
grep -c txt *.sgml # (количество совпадений с "txt" в "*.sgml" файлах)
# grep -cz .
#          ^ точка
# означает подсчет (-c) непустых ("." -- содержащих хотя бы один символ) элементов,
# разделённых нулевыми байтами (-z)
#
printf 'a b\nc d\n\n\n\n\n\000\n\000e\000\000\nf' | grep -cz . # 4
printf 'a b\nc d\n\n\n\n\n\000\n\000e\000\000\nf' | grep -cz '$' # 5
printf 'a b\nc d\n\n\n\n\n\000\n\000e\000\000\nf' | grep -cz '^' # 5
#
printf 'a b\nc d\n\n\n\n\n\000\n\000e\000\000\nf' | grep -c '$' # 9
# По-умолчанию, в качестве разделителя, принимается символ перевода строки (\n).
# Обратите внимание: ключ -z характерен для GNU-версии "grep".
# Спасибо S.C.

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

bash$ grep Linux osinfo.txt misc.txt 
osinfo.txt:This is a file containing information about Linux.
osinfo.txt:The GPL governs the distribution of the Linux operating system.
misc.txt:The Linux operating system is steadily gaining in popularity.

Для того, чтобы заставить grep выводить имя файла, когда поиск производится по одному-единственному файлу, достаточно указать устройство /dev/null в качестве второго файла.

bash$ grep Linux osinfo.txt /dev/null
osinfo.txt:This is a file containing information about Linux.
osinfo.txt:The GPL governs the distribution of the Linux operating system.

Если совпадение было найдено, то grep возвращает код завершения — 0, это может оказаться полезным при выполнении поиска в условных операторах ( в таких случаях особый интерес может представлять ключ -q, который подавляет вывод).

SUCCESS=0 # если найдено совпадение
word=Linux
filename=data.file
grep -q "$word" "$filename" # "-q" — подавляет вывод на stdout.
if [ $? -eq $SUCCESS ]
# if grep -q "$word" "$filename" эта комбинация может заменить строки 5 - 7.
then
 echo "Образец $word найден в $filename"
else
 echo "Образец $word в файле $filename не найден"
fi

Пример: Сценарий-эмулятор "grep".

#!/bin/bash
# grp.sh: Очень "грубая" реализация 'grep'.
E_BADARGS=65
if [ -z "$1" ] # Проверка наличия аргументов.
then
 echo "Порядок использования: `basename $0` pattern"
 exit $E_BADARGS
fi
echo
for file in * # Обход всех файлов в $PWD.
do
 output=$(sed -n /"$1"/p $file) # Подстановка команд.
 if [ ! -z "$output" ] # Что произойдет, если кавычки вокруг "$output" убрать?
 then
  echo -n "$file: "
  echo $output
 fi # эквивалент: sed -ne "/$1/s|^|${file}: |p"
 echo
done
echo
exit 0
# Упражнения:
# ---------
# 1) Добавьте вывод символов перевода строки, если найдено более одного совпадения в любом из файлов.
# 2) Добавьте обработку различных ключей.

egrep — то же самое, что и grep -E. Эта команда использует несколько отличающийся, расширенный набор регулярных выражений, что позволяет выполнять поиск более гибко.
fgrep — то же самое, что и grep -F. Эта команда выполняет поиск строк символов (не регулярных выражений), что несколько увеличивает скорость поиска.
Утилита agrep имеет более широкие возможности поиска приблизительных совпадений. Образец поиска может отличаться от найденой строки на указанное число символов.
Для поиска по сжатым файлам следует использовать утилиты zgrep, zegrep или zfgrep. Они с успехом могут использоваться и для не сжатых файлов, но в этом случае они уступают в скорости обычным grep, egrep и fgrep. Они очень удобны при выполнении поиска по смешенному набору файлов — когда одни файлы сжаты, а другие нет.
Для поиска по bzip-файлам используйте bzgrep.
look
Команда look очень похожа на grep, и предназначена для поиска по "словарям" — отсортированным файлам. По-умолчанию, поиск выполняется в файле /usr/dict/words, но может быть указан и другой словарь.

Пример: Поиск слов в словаре.

#!/bin/bash
# lookup: Выполняется поиск каждого слова из файла в словаре.
file=words.data # Файл с искомыми словами.
echo
while [ "$word" != end ] # Последнее слово в файле.
do
 read word # Из файла, потому, что выполнено перенаправление в конце цикла.
 look $word > /dev/null # Подавление вывода строк из словаря.
 lookup=$? # Код возврата команды 'look'.
 if [ "$lookup" -eq 0 ]
 then
  echo "Слово \"$word\" найдено."
 else
  echo "Слово \"$word\" не найдено."
 fi
done <"$file" # Перенаправление ввода из файла $file, так что "чтение" производится оттуда.
echo
exit 0
# ----------------------------------------------------------------
# Строки, расположенные ниже не будут исполнены, поскольку выше стоит команда "exit".
# Stephane Chazelas предложил более короткий вариант:
while read word && [[ $word != end ]]
do if look "$word" > /dev/null
 then echo "Слово \"$word\" найдено."
 else echo "Слово \"$word\" не найдено."
 fi
done <"$file"
exit 0

sed,awk
Скриптовые языки, специально разработанные для анализа текстовых данных. sed — Неинтерактивный "потоковый редактор". Широко используется в сценариях на языке командной оболочки.
awk — Утилита контекстного поиска и преобразования текста, замечательный инструмент для извлечения и/или обработки полей (колонок) в структурированных текстовых файлах. Синтаксис awk напоминает язык C.
wc
wc — "word count", счётчик слов в файле или в потоке:

bash $ wc /usr/doc/sed-3.02/README
20 127 838 /usr/doc/sed-3.02/README
[20 строк 127 слов 838 символов]

wc -w — подсчитывает только слова.
wc -l — подсчитывает только строки.
wc -c — подсчитывает только символы.
wc -L — возвращает длину наибольшей строки.

Подсчёт количества .txt-файлов в текущем каталоге с помощью wc:

$ ls *.txt | wc -l
# Эта команда будет работать, если ни в одном из имен файлов "*.txt" нет символа перевода строки.
# Альтернативный вариант:
# find . -maxdepth 1 -name \*.txt -print0 | grep -cz .
# (shopt -s nullglob; set -- *.txt; echo $#)
 
# Спасибо S.C.

Подсчёт общего размера файлов, чьи имена начинаются с символов, в диапазоне d—h

bash$ wc [d-h]* | grep total | awk '{print $3}'
71832

От переводчика: в случае, если у вас локаль отлична от "C", то вышеприведённая команда может не дать результата, поскольку wc вернёт не слово "total", в конце вывода, а "итого". Тогда можно попробовать несколько изменённый вариант:

bash$ wc [d-h]* | grep итого | awk '{print $3}'
71832

Использование wc для подсчёта количества вхождений слова "Linux" в основной исходный файл с текстом этого руководства.

bash$ grep Linux abs-book.sgml | wc -l
50

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

... | grep foo | wc -l

Ключ -c (--count) команды grep. Спасибо S.C.:

... | grep -c foo

tr
Замена одних символов на другие. В отдельных случаях символы необходимо заключать в кавычки и/или квадратные скобки. Кавычки предотвращают интерпретацию специальных символов командной оболочкой. Квадратные скобки должны заключаться в кавычки. Команда tr "A-Z" "*" < filename или tr A-Z \* < filename заменяет все символы верхнего регистра в filename на звёздочки (вывод производится на stdout). В некоторых системах этот вариант может оказаться неработоспособным, тогда попробуйте tr A-Z '[**]' .
Ключ -d удаляет символы из заданного диапазона.

echo "abcdef" # abcdef
echo "abcdef" | tr -d b-d # aef
 
tr -d 0-9 <filename # Удалит все цифровые символы из файла "filename".

Ключ --squeeze-repeats (-s) удалит все повторяющиеся последовательности символов. Может использоваться для удаления лишних пробельных символов.

bash$ echo "XXXXX" | tr --squeeze-repeats 'X'
X

Ключ -c "complement" заменит символы в соответствии с шаблоном. Этот ключ воздействует только на те символы, которые НЕ соответствуют заданному шаблону.

bash$ echo "acfdeb123" | tr -c b-d +
+c+d+b++++

Обратите внимание: команда tr корректно распознает символьные классы POSIX.

bash$ echo "abcd2ef1" | tr '[:alpha:]' -
----2--1

Пример: toupper: Преобразование символов в верхний регистр.

#!/bin/bash
# Преобразование символов в верхний регистр.
E_BADARGS=65
if [ -z "$1" ] # Стандартная проверка командной строки.
then
 echo "Порядок использования: `basename $0` filename"
 exit $E_BADARGS
fi
tr a-z A-Z <"$1"
# Тот же эффект можно получить при использовании символьных классов POSIX:
# tr '[:lower:]' '[:upper:]' <"$1"
# Спасибо S.C.
exit 0

Пример: lowercase: Изменение имен всех файлов в текущем каталоге в нижний регистр.

#! /bin/bash
#
# Изменит все имена файлов в текущем каталоге в нижний регистр.
#
for filename in * # Обход всех файлов в каталоге.
do
 fname=`basename $filename`
 n=`echo $fname | tr A-Z a-z` # Перевести символы в нижний регистр.
 if [ "$fname" != "$n" ] # Переименовать только те файлы, имена которых изменились.
 then
  mv $fname $n
 fi
done
exit 0
# Строки приведённые ниже не будут исполняться, поскольку выше стоит команда "exit".
#--------------------------------------------------------#
# Запустите эту часть сценария, удалив строки , стоящие выше.
# Сценарий, приведенный выше, не работает с именами файлов, содержащими пробелы или символы перевода строки.
# В связи с этим, Stephane Chazelas предложил следующий вариант:
for filename in * # Нет необходимости использовать basename,
                  # поскольку "*" возвращает имена, не содержащие "/".
do n=`echo "$filename/" | tr '[:upper:]' '[:lower:]'`
# символьные классы POSIX.
# Завершающий слэш добавлен для того, чтобы символ перевода строки
# не был удален при подстановке команды.
# Подстановка переменной:
n=${n%/} # Удаление завершающего слэша, добавленного выше.
[[ $filename == $n ]] || mv "$filename" "$n"
# Проверка — действительно ли изменилось имя файла.
done
exit 0

Пример: du: Преобразование текстового файла из формата DOS в формат Unix.

#!/bin/bash
# du.sh: Преобразование текстового файла из формата DOS в формат UNIX.
E_WRONGARGS=65
if [ -z "$1" ]
then
 echo "Порядок использования: `basename $0` filename-to-convert"
 exit $E_WRONGARGS
fi
NEWFILENAME=$1.unxCR='\015' # Возврат каретки.
                            # 015 — это символ CR в восьмеричном формате
# Строки в текстовых файлах DOS завершаются комбинацией символов CR-LF.
tr -d $CR < $1 > $NEWFILENAME
# Удалить символы CR и записать в новый файл.
echo "Исходный текстовый файл: \"$1\"."
echo "Преобразованный файл: \"$NEWFILENAME\"."
exit 0
# Упражнение:
# --------
# Измените этот сценарий таким образом, чтобы он преобразовывал файлы из
# формата UNIX в формат DOS.

Пример: rot13: Сверхслабое шифрование по алгоритму rot13.

#!/bin/bash
# rot13.sh: Классический алгоритм шифрования rot13,
# который способен "расколоть" даже 3-х летний ребенок.
# Порядок использования: ./rot13.sh filename
# или ./rot13.sh <filename
# или ./rot13.sh и ввести текст с клавиатуры (stdin)
cat "$@" | tr 'a-zA-Z' 'n-za-mN-ZA-M' # "a" заменяется на "n", "b" на "o", и т.д.
# Конструкция 'cat "$@"'
# позволяет вводить данные как со stdin, так и из файла.
exit 0

Пример: Более "сложный" шифр

#!/bin/bash
# crypto-quote.sh: Ограниченное шифрование
# Шифрование ограничивается простой заменой одних алфавитных символов другими.
# Результат очень похож на шифры-загадки
key=ETAOINSHRDLUBCFGJMQPVWZYXK
# Здесь, "key" — ни что иное, как "перемешанный" алфавит.
# Изменение ключа "key" приведет к изменению шифра.
# Конструкция 'cat "$@"' позволяет вводить данные как со stdin, так и из файла.
# Если используется stdin, то ввод должен завершаться комбинацией Control-D.
# Иначе, в командной строке, сценарию должно быть передано имя файла.
cat "$@" | tr "a-z" "A-Z" | tr "A-Z" "$key"
#        |в верхний регистр| шифрование
# Такой прием позволяет шифровать как символы в верхнем регистре, так и в нижнем.
# Неалфавитные символы остаются без изменений.
 
# Попробуйте зашифровать какой либо текст, например
# "Nothing so needs reforming as other people's habits." — Mark Twain
#
# Результат будет:
# "CFPHRCS QF CIIOQ MINFMBRCS EQ FPHIM GIFGUI'Q HETRPQ." — BEML PZERC
# Для дешифрации можно использовать следующую комбинацию:
# cat "$@" | tr "$key" "A-Z"
# Этот нехитрый шифр может быть "взломан" 12-ти летним ребёнком
# с помощью карандаша и бумаги.
exit 0

Различные версии tr
Утилита tr имеет две, исторически сложившиеся, версии. BSD-версия не использует квадратные скобки (tr a-z A-Z), в то время как SysV-версия использует их (tr '[a-z]' '[A-Z]'). GNU-версия утилиты tr напоминает версию BSD, но диапазоны символов обязательно должны заключаться в квадратные скобки.

fold
Выравнивает текст по ширине, разрывая, если это необходимо, слова. Особый интерес представляет ключ -s, который производит перенос строк по пробелам, стараясь не разрывать слова.
fmt
Очень простая утилита форматирования текста, чаще всего используемая как фильтр в конвейерах для того, чтобы выполнить "перенос" длинных строк текста.

Пример: Отформатированный список файлов.

#!/bin/bash
WIDTH=40 # 40 символов в строке.
b=`ls /usr/local/bin` # Получить список файлов...
echo $b | fmt -w $WIDTH
# То же самое можно выполнить командой
# echo $b | fold - -s -w $WIDTH
exit 0

Очень мощной альтернативой утилите fmt, является утилита par (автор Kamil Toman), которую вы сможете найти на http://www.cs.berkeley.edu/~amc/Par/. col
Эта утилита с обманчивым названием удаляет из входного потока символы обратной подачи бумаги (код ESC 7). Она так же пытается заменить пробелы на табуляции. Основная область применения утилиты col — фильтрация вывода отдельных утилит обработки текста, таких как groff и tbl.
col
Форматирование по столбцам. Эта утилита преобразует текст, например какой либо список, в табличное, более "удобочитаемое", представление, вставляя символы табуляции по мере необходимости.

Пример: Пример форматирования списка файлов в каталоге.

#!/bin/bash
# За основу сценария взят пример "man column".
(printf "PERMISSIONS LINKS OWNER GROUP SIZE DATE TIME PROG-NAME\n" \
; ls -l | sed 1d) | column -t   # Команда "sed 1d" удаляет первую строку, выводимую командой ls,
                                # (для локали "С" это строка: "total N", где "N" — общее количество файлов.
# Ключ -t, команды "column", означает "табличное" представление.
exit 0

colrm
Утилита удаления колонок. Удаляет колонки (столбцы) символов из файла и выводит результат на stdout.
colrm 2 4 < filename — удалит символы со 2-го по 4-й включительно, в каждой строке в файле filename.
Если файл содержит символы табуляции или непечатаемые символы, то результат может получиться самым неожиданным. В таких случаях, как правило, утилиту colrm, в конвейере, окружают командами expand и unexpand.
nl
Нумерует строки в файле. nl filename — выведет файл filename на stdout, и в начале каждой строки вставит её порядковый номер, счёт начинается с первой непустой строки. Если файл не указывается, то принимается ввод со stdin. Вывод команды nl очень напоминает cat -n, однако, по-умолчанию nl не нумерует пустые строки.

Пример: Самонумерующийся сценарий.

#!/bin/bash
# Сценарий выводит себя сам на stdout дважды, нумеруя строки сценария.
# 'nl' вставит для этой строки номер 3, поскольку она не нумерует пустые строки.
# 'cat -n' вставит для этой строки номер 5.
nl `basename $0`
echo; echo # А теперь попробуем вывести текст сценария с помощью 'cat -n'cat -n `basename $0`
# Различия состоят в том, что 'cat -n' нумерует все строки.
# Обратите внимание: 'nl -ba' — сделает то же самое.
exit 0

pr
Подготовка файла к печати. Утилита производит разбивку файла на страницы, приводя его в вид пригодный для печати или для вывода на экран. Разнообразные ключи позволяют выполнять различные манипуляции над строками и колонками, соединять строки, устанавливать поля, нумеровать строки, добавлять колонтитулы и многое, многое другое. Утилита pr соединяет в себе функциональность таких команд, как nl, paste, fold, column и expand.
Следующая команда выдаст хорошо оформленное и разбитое на страницы содержимое файла fileZZZ:

pr -o 5 --width=65 fileZZZ | more

Хочу особо отметить ключ -d, который выводит строки с двойным интервалом (тот же эффект,что и sed -G).
gettext
GNU утилита, предназначена для нужд локализации и перевода сообщений программ, выводимых на экран, на язык пользователя. Не смотря на то, что это актуально, прежде всего,для программ на языке C, тем не менее gettext с успехом может использоваться в сценариях командной оболочки для тех же целей.
msgfmt
Эта утилита предназначена для создания двоичных файлов с переводом сообщений, выводимых перед пользователем. Используется для нужд локализации.
iconv
Утилита преобразования текста из одной кодировки в другую. В основном используется для нужд локализации.
recode
Может рассматриваться как разновидность утилиты iconv, описанной выше. Универсальная утилита для преобразования текстовой информации в различные кодировки.
TeX,gs
TeX и Postscript — языки разметки текста, используемые для подготовки текста к печати или выводу на экран.
TeX — это сложная система подготовки к печати, разработанная Дональдом Кнутом (Donald Knuth). Эту утилиту удобнее использовать внутри сценария, чем в командной строке, поскольку в сценарии проще один раз записать все необходимые параметры, передаваемые утилите, для получения необходимого результата.
Ghostscript (gs) — это GPL-версия интерпретатора Postscript.
groff,tbl,eqn
groff — это ещё один язык разметки текста и форматированного вывода. Является расширенной GNU-версией пакета roff/troff в Unix-системах.
tbl — утилита обработки таблиц, должна рассматриваться как составная часть groff, так как её задачей является преобразование таблиц в команды groff.
eqn — утилита преобразования математических выражений в команды groff.
lex,yacc
lex — утилита лексического разбора текста. В Linux-системах заменена на свободно распространяемую утилиту flex.
yacc — утилита для создания синтаксических анализаторов, на основе набора грамматик, задаваемых разработчиком. В Linux-системах, эта утилита заменена на свободнораспространяемую утилиту bison.

Команды для работы с файлами и архивами

Архивация
tar
Стандартная, для Unix, утилита архивирования. Первоначально — это была программа TapeARchiving, которая впоследствии переросла в универсальный пакет, который может работать с любыми типами устройств. В GNU-версию tar была добавлена возможность одновременно производить сжатие tar-архива, например команда tar czvf archive_name.tar.gz* создаёт tar-архив дерева подкаталогов и вызывает gzip для выполнения сжатия, исключение составляют скрытые файлы в текущем каталоге ($PWD).
Некоторые, часто используемые, ключи команды tar:

  1. -c — создать (create) новый архив
  2. -x — извлечь (extract) файлы из архива
  3. --delete — удалить (delete) файлы из архива. Этот ключ игнорируется для накопителей на магнитной ленте.
  4. -r — добавить (append) файлы в существующий архив
  5. -A — добавить (append) tar-файлы в существующий архив
  6. -t — список файлов в архиве (содержимое архива)
  7. -u — обновить (update) архив
  8. -d — операция сравнения архива с заданной файловой системой
  9. -z — обработка архива с помощью gzip. Сжатие или разжатие, в зависимости от комбинации сопутствующих ключей -c или -x
  10. -j — обработка архива с помощью bzip2

При восстановлении "битых" tar.gz архивов могут возникнуть определённые сложности, поэтому делайте несколько резервных копий. shar
Утилита создания shell-архива. Архивируемые файлы объединяются в единый файл без выполнения сжатия, в результате получается архив — по сути полноценный сценарий на языке командной оболочки, начинающийся со строки #!/bin/sh, который содержит полный набор команд, необходимый для разархивирования. Такого рода архивы до сих пор можно найти в некоторых телеконференциях в Internet, но в последнее время они активно вытесняются связкой tar/gzip. Для распаковки shar-архивов предназначена команда unshar.
ar
Утилита создания и обслуживания архивов, главным образом применяется к двоичным файлам библиотек.
rpm
Red Hat Package Manager, или rpm — набор утилит, предназначенных для построения и обслуживания пакетов программного обеспечения как в исходном коде, так и в собранном(откомпилированном) виде. Среди всего прочего, включает в себя утилиты, производящие установку ПО, проверку зависимостей пакетов и проверку их целостности.
Самый простой вариант установки ПО из rpm — выполнить команду rpm -ipackage_name.rpm. Команда rpm -qa выдаст полный список всех установленных rpm-пакетов в данной системе. Команда rpm -qa package_name выведет только пакет(ы) с именем, содержащим комбинацию символов package_name.

bash$ rpm -qa
redhat-logos-1.1.3-1
glibc-2.2.4-13 
cracklib-2.7-12
dosfstools-2.7-1
gdbm-1.8.0-10
ksymoops-2.4.1-1 
mktemp-1.5-11 
perl-5.6.0-17
reiserfs-utils-3.x.0j-2
...
bash$ rpm -qa docbook-utils
docbook-utils-0.6.9-2
bash$ rpm -qa docbook | grep docbook
docbook-dtd31-sgml-1.0-10
docbook-style-dsssl-1.64-3
docbook-dtd30-sgml-1.0-10
docbook-dtd40-sgml-1.0-11
docbook-utils-pdf-0.6.9-2
docbook-dtd41-sgml-1.0-10
docbook-utils-0.6.9-2

cpio
Специализированная утилита архивации и копирования (copy input and output). Используется всё реже и реже, поскольку вытесняется более мощным архиватором tar/gzip. Наиболее употребительна для таких операций, как перемещение дерева каталогов.

Пример: Пример перемещения дерева каталогов с помощью cpio.

#!/bin/bash
# Копирование дерева каталогов с помощью cpio.
ARGS=2
E_BADARGS=65
if [ $# -ne "$ARGS" ]
then
 echo "Порядок использования: `basename $0` source destination"
 exit $E_BADARGS
fi
source=$1
destination=$2
find "$source" -depth | cpio -admvp "$destination"
# Информацию по ключам утилиты cpio вы найдете в страницах руководства "man cpio".
exit 0

rpm2cpio
Эта утилита конвертирует rpm-пакет в архив cpio.

Пример: Распаковка архива rpm.

#!/bin/bash
# de-rpm.sh: Распаковка архива 'rpm'
: ${1?"Порядок использования: `basename $0` target-file"}
# Сценарию должно быть передано имя архива 'rpm'.
TEMPFILE=$$.cpio          # Временный файл с "уникальным" именем.
                          # $$ — PID процесса сценария.
rpm2cpio < $1 > $TEMPFILE # Конверсия из rpm в cpio.
cpio --make-directories -F $TEMPFILE -i # Распаковка cpio-архива.
rm -f $TEMPFILE           # Удаление cpio-архива.
exit 0
# Упражнение:
# Добавьте проверку на: 
# 1) Существование "target-file"
# 2) Действительно ли "target-file" является rpm-архивом.
# Подсказка: используйте команду 'file'.

Сжатие
gzip
Стандартная GNU/Unix утилита сжатия, заменившая более слабую, и к тому же проприетарную, утилиту compress. Соответствующая утилита декомпрессии (разжатия) — gunzip, которая является эквивалентом команды gzip -d.
Для работы со сжатыми файлами в конвейере используется фильтр zcat, который выводит результат своей работы на stdout, допускает перенаправление вывода. Фактически это та же команда cat, только приспособленная для работы со сжатыми файлами (включая файлы, сжатые утилитой compress). Эквивалент команды zcatgzip -dc.
В некоторых коммерческих версиях Unix, команда zcat является синонимом команды uncompress -c, и не может работать с файлами, сжатыми с помощью gzip.
bzip2
Альтернативная утилита сжатия, обычно даёт более высокую степень сжатия (но при этом работает медленнее), чем gzip, особенно это проявляется на больших файлах. Соответствующая утилита декомпрессии — bunzip2. В современные версии tar добавлена поддержка bzip2.
compress,uncompress
Устаревшие проприетарные утилиты для работы с архивами, входящие в состав некоторых коммерческих дистрибутивов Unix. В последнее время вытесняются более мощной утилитой gzip. Linux-дистрибутивы, как правило, включают в свой состав эти утилиты для обратной совместимости, однако gunzip корректно разархивирует файлы, обработанные с помощью compress.
Утилита znew предназначена для преобразования compress-архивов в gzip-архивы.
sq
Ещё одна утилита-фильтр сжатия, которая обслуживает только отсортированные списки слов. Использует стандартный, для фильтров, синтаксис вызова — sq < input-file > output-file. Быстрая, но не такая эффективная как gzip. Соответствующая ей утилита декомпрессии называется unsq, синтаксис вызова аналогичен утилите sq.
Вывод от sq может быть передан по конвейеру утилите gzip, для дальнейшего сжатия.
zip,unzip
Кроссплатформенная утилита архивирования и сжатия, совместимая, по формату архивного файла, с утилитой DOS — pkzip.exe. "Zip"-архивы, по-моему, более приемлемый вариант для обмена данными через Internet, чем "tarballs" (тарболлы, или tar-архивы).
unarc,unarj,unrar
Этот набор утилит предназначен для распаковки архивов, созданных с помощью DOS-архиваторов — arc.exe, arj.exe и rar.exe.
Получение сведений о файлах
file
Утилита идентификации файлов. Команда file filename вернёт тип файла filename, например, ascii, text или data. Для этого она анализирует сигнатуру, или магическое число и сопоставляет её со списком известных сигнатур из /usr/share/magic, /etc/magic или /usr/lib/magic (в зависимости от дистрибутива Linux/Unix).
-f — ключ пакетного режима работы утилиты file, в этом случае утилита принимает список анализируемых имён файлов из заданного файла. Ключ -z используется для анализа файлов в архиве.

bash$ file test.tar.gz
test.tar.gz: gzip compressed data, deflated, last modified: Sun Sep 16 13:34:51 2001, os:
Unix
bash file -z test.tar.gz
test.tar.gz: GNU tar archive (gzip compressed data, deflated, last modified: Sun Sep 16 13:34:51 2001, os: Unix)

Пример: Удаление комментариев из файла с текстом программы на языке C.

#!/bin/bash
# strip-comment.sh: Удаление комментариев (/* COMMENT */) из исходных текстов программ на языке C.
E_NOARGS=65
E_ARGERROR=66
E_WRONG_FILE_TYPE=67
if [ $# -eq "$E_NOARGS" ]
then
 echo "Порядок использования: `basename $0` C-program-file" >&2 # Вывод сообщения на stderr.
exit $E_ARGERROR
fi
# Проверка типа файла.
type=`eval file $1 | awk '{ print $2, $3, $4, $5 }'`
# "file $1" — выводит тип файла...
# затем awk удаляет первое поле — имя файла...
# после этого результат записывается в переменную "type".
correct_type="ASCII C program text"
 
if [ "$type" != "$correct_type" ]
then
 echo
 echo "Этот сценарий работает только с исходными текстами программ на языке C."
 echo
 exit $E_WRONG_FILE_TYPE
fi
 
# Довольно замысловатый сценарий sed :
#--------
sed '
/^\/\*/d
/.*\/\*/d
' $1
#--------
# Если вы потратите несколько часов на изучение основ sed, то он станет немного понятнее.
# Следовало бы добавить ещё обработку
# комментариев, расположенных в одной строке с кодом.
# Оставляю это вам, в качестве упражнения.
# Кроме того, этот сценарий удалит все строки, которые содержат комбинации символов "*/" или "/*",
# не всегда желаемый результат.
exit 0
# ----------------------------------------------------------------
# Строки, расположенные ниже не будут исполнены из-за стоящей выше команды 'exit 0'.
# Stephane Chazelas предложил другой, альтернативный вариант:
usage() {
 echo "Порядок использования: `basename $0` C-program-file" >&2
 exit 1
}
WEIRD=`echo -n -e '\377'` # или WEIRD=$'\377'
[[ $# -eq 1 ]] || usage
case `file "$1"` in
 *"C program text"*) sed -e "s%/\*%${WEIRD}%g;s%\*/%${WEIRD}%g" "$1" \
 | tr '\377\n' '\n\377' \
 | sed -ne 'p;n' \
 | tr -d '\n' | tr '\377' '\n';;
 *) usage;;
esac
# Этот вариант, все ещё некорректно обрабатывает такие строки как:
# printf("/*");
# или
# /* /* ошибочный вложенный комментарий */
#
# Для обработки специальных случаев (\", \\" ...) придется написать синтаксический анализатор
# (может быть с помощью lex или yacc?).
exit 0

which
Команда which command-xxx вернёт полный путь к command-xx. Очень полезна для того,чтобы узнать — установлена ли та или иная утилита в системе.

$bash which rm
/usr/bin/rm

whereis
Очень похожа на which, упоминавшуюся выше. Команда whereis command-xxx вернёт полный путь к "command-xxx", но кроме того, ещё и путь к manpage — файлу, странице справочника по заданной утилите.

$bash whereis rm
rm: /bin/rm /usr/share/man/man1/rm.1.bz2

whatis
Утилита whatis filexxx отыщет "filexxx" в своей базе данных. Может рассматриваться как упрощённый вариант команды man.

$bash whatis whatis
whatis (1) - search the whatis database for complete words

Пример: Исследование каталога /usr/X11R6/bin.

#!/bin/bash
# Что находится в каталоге /usr/X11R6/bin?
DIRECTORY="/usr/X11R6/bin"
# Попробуйте также "/bin", "/usr/bin", "/usr/local/bin", и т.д.
for file in $DIRECTORY/*
do
 whatis `basename $file` # Вывод информации о файле.
done
exit 0
# Вывод этого сценария можно перенаправить в файл:
# ./what.sh >>whatis.db
# или включить постраничный просмотр на экране,
# ./what.sh | less

vdir
Вывод списка файлов в каталоге. Тот же эффект имеет команда ls -l
Это одна из утилит GNU fileutils.

bash$ vdir
total 10
-rw-r--r-- 1 bozo bozo 4034 Jul 18 22:04 data1.xrolo
-rw-r--r-- 1 bozo bozo 4602 May 25 13:58 data1.xrolo.bak
-rw-r--r-- 1 bozo bozo 877 Dec 17 2000 employment.xrolo
bash ls -l
total 10
-rw-r--r-- 1 bozo bozo 4034 Jul 18 22:04 data1.xrolo
-rw-r--r-- 1 bozo bozo 4602 May 25 13:58 data1.xrolo.bak
-rw-r--r-- 1 bozo bozo 877 Dec 17 2000 employment.xrolo

locate,slocate
Команда locate определяет местонахождение файла, используя свою базу данных, создаваемую специально для этих целей. Команда slocate — это защищенная версия locate (которая может оказаться простым псевдонимом команды slocate).

$bash locate hickson
/usr/lib/xephem/catalogs/hickson.edb

readlink
Возвращает имя файла, на который указывает символическая ссылка.

bash$ readlink /usr/bin/awk
../../bin/gawk

strings
Команда strings используется для поиска печатаемых строк в двоичных файлах. Она выводит последовательности печатаемых символов, обнаруженных в заданном файле. Может использоваться для прикидочного анализа дамп-файлов (core dump) или для отыскания информации о типе файла, например для графических файлов неизвестного формата(например, strings image-file | more может вывести такую строчку: JFIF, что говорит о том, что мы имеем дело с графическим файлом в формате jpeg). В сценариях, вероятнее всего, вам придётся использовать эту команду в связке с grep или sed.

Пример: "Расширенная" команда strings.

#!/bin/bash
# wstrings.sh: "word-strings" (расширенная команда "strings")
#
# Этот сценарий фильтрует вывод команды "strings" путем проверки на соответствие
# выводимых слов по файлу словаря.
# Таким способом эффективно "отсекается" весь "мусор",
# и выводятся только распознанные слова.
# =================================================================
# Стандартная проверка входных аргументов
ARGS=1
E_BADARGS=65
E_NOFILE=66
if [ $# -ne $ARGS ]
then
 echo "Порядок использования: `basename $0` filename"
 exit $E_BADARGS
fi
if [ ! -f "$1" ] # Проверка наличия файла.
then
 echo "Файл \"$1\" не найден."
 exit $E_NOFILE
fi
# =================================================================
MINSTRLEN=3                             # Минимальная длина строки.
WORDFILE=/usr/share/dict/linux.words    # Файл словаря.
                                        # Можно указать иной
                                        # файл словаря
                                        # в формате — "одно слово на строке".
wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \
tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '`
# Трансляция вывода от 'strings' с помощью нескольких 'tr'.
# "tr A-Z a-z" — перевод в нижний регистр.
# "tr '[:space:]'" — конвертирует пробелы в символы Z.
# "tr -cs '[:alpha:]' Z" — конвертирует неалфавитные символы в символы Z,
# и удаляет повторяющиеся символы Z.
# "tr -s '\173-\377' Z" — Конвертирует все символы, с кодами выше 'z' в Z
# и удаляет повторяющиеся символы Z,
# эта команда удалит все символы, которые не были распознаны предыдущими
# командами трансляции (tr).
# Наконец, "tr Z ' '" — преобразует все символы Z в пробелы,
# которые будут рассматриваться в качестве разделителя слов в цикле, приведенном ниже.
 
# Обратите внимание на технику многоуровневой обработки с помощью 'tr',
# каждый раз эта команда вызывается с различным набором аргументов.
for word in $wlist                   # Важно:
                                     # переменная $wlist не должна заключаться в кавычки.
                                     # "$wlist" — не сработает.
                                     # Почему?
do
 strlen=${#word}                   # Дина строки.
 if [ "$strlen" -lt "$MINSTRLEN" ] # Не рассматривать короткие строки.
 then
  continue
 fi
 grep -Fw $word "$WORDFILE"        # Проверка слова по словарю.
done
exit 0

Сравнение
diff,patch
diff: очень гибкая утилита сравнения файлов. Она выполняет построчное сравнение файлов. В отдельных случаях, таких как поиск по словарю, может оказаться полезной фильтрация файлов с помощью sort и uniq перед тем как отдать поток данных через конвейер утилите diff. diff file-1 file-2 — выведет строки, имеющие отличия, указывая — какому файлу, какая строка принадлежит.
С ключом --side-by-side, команда diff выведет сравниваемые файлы в две колонки, с указанием несовпадающих строк. Ключи -c и -u так же служат для облегчения интерпретации результатов работы diff. Существует ряд интерфейсных оболочек для утилиты diff, среди них можно назвать: spiff, wdiff,xdiff и mgdiff. Команда diff возвращает код завершения 0, если сравниваемые файлы идентичны и 1, если они отличаются. Это позволяет использовать diff в условных операторах внутри сценариев на языке командной оболочки (см.ниже).
В общем случае, diff используется для генерации файла различий, который используется как аргумент команды patch. Ключ -e отвечает за вывод файла различий в формате, пригодном для использования с ed или ex.
patch: гибкая утилита для "наложения заплат". С помощью файла различий, сгенерированного утилитой diff, утилита patch может использоваться для обновления устаревших версий файлов. Это позволяет распространять относительно небольшие "diff"-файлы вместо целых пакетов. Распространение "заплат" к ядру стало наиболее предпочтительным методом распространения более новых версий ядра Linux.

patch -p1 < patch-file 
# Применит все изменения из 'patch-file'
# к файлам, описанным там же.
# Так выполняется обновление пакетов до более высоких версий.

Наложение "заплат" на ядро:

cd /usr/src
gzip -cd patchXX.gz | patch -p0
# Обновление исходных текстов ядра с помощью 'patch'.
# Пример взят из файла "README",
# автор не известен (Alan Cox?).

Кроме того, утилита diff в состоянии выполнять рекурсивный обход каталогов.

bash$ diff -r ~/notes1 ~/notes2
Only in /home/bozo/notes1: file02
Only in /home/bozo/notes1: file03
Only in /home/bozo/notes2: file04

Утилита zdiff сравнивает сжатые, с помощью gzip, файлы.
diff3
Расширенная версия diff, которая сравнивает сразу 3 файла. В случае успеха возвращает 0, но,к сожалению, не даёт никакой информации о результатах сравнения.

bash$ diff3 file-1 file-2 file-3
====
1:1c
 This is line 1 of "file-1".
2:1c
 This is line 1 of "file-2".
3:1c
 This is line 1 of "file-3"

sdiff
Сравнение и/или редактирование двух файлов перед объединением их в один файл. Это интерактивная утилита, по своей природе, и из-за этого она довольно редко используется в сценариях.
cmp
Утилита cmp — это упрощенная версия diff. В то время, как diff выводит подробную информацию об имеющихся различиях, утилита cmp лишь показывает номер строки и позицию в строке, где было встречено различие. Подобно команде diff, команда cmp возвращает код завершения 0, если файлы идентичны и 1, если они различны. Это позволяет использовать команду cmp в условных операторах.

Пример: Пример сравнения двух файлов с помощью cmp.

#!/bin/bash
ARGS=2 # Ожидаются два аргумента командной строки.
E_BADARGS=65
E_UNREADABLE=66
if [ $# -ne "$ARGS" ]
then
 echo "Порядок использования: `basename $0` file1 file2"
 exit $E_BADARGS
fi
if [[ ! -r "$1" || ! -r "$2" ]]
then
 echo "Оба файла должны существовать и должны быть доступны для чтения."
 exit $E_UNREADABLE
fi
cmp $1 $2 &> /dev/null # /dev/null — "похоронит" вывод от команды "cmp".
# cmp -s $1 $2 даст тот же результат ("-s" — флаг "тишины" для "cmp")
# Спасибо Anders Gustavsson за замечание.
#
# Также применимо к 'diff', т.е., diff $1 $2 &> /dev/null
if [ $? -eq 0 ] # Проверка кода возврата команды "cmp".
then
 echo "Файл \"$1\" идентичен файлу \"$2\"."
else
 echo "Файл \"$1\" отличается от файла \"$2\"."
fi
exit 0

Для работы с gzip файлами используется утилита zcmp.
comm
Универсальная утилита сравнения. Работает с отсортированными файлами.

comm -options first-file second-file

comm file-1 file-2

вывод в три колонки:

  • колонка 1 = уникальные строки для file-1
  • колонка 2 = уникальные строки для file-2
  • колонка 3 = одинаковые строки.

Ключи, подавляющие вывод в одной или более колонках:

  • -1 — подавление вывода в колонку 1
  • -2 — подавление вывода в колонку 2
  • -3 — подавление вывода в колонку 3
  • -12 — подавление вывода в колонки 1 и 2, и т.д.

Утилиты
basename
Выводит только название файла, без каталога размещения. Конструкция basename $0 — позволяет сценарию узнать свое имя, то есть имя файла, который был запущен. Это имя может быть использовано для вывода сообщений, например:

echo "Порядок использования: `basename $0` arg1 arg2 ... argn"

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

Пример: Утилиты basename и dirname.

#!/bin/bash
a=/home/bozo/daily-journal.txt
echo "Basename для /home/bozo/daily-journal.txt = `basename $a`"
echo "Dirname для /home/bozo/daily-journal.txt = `dirname $a`"
echo
echo "Мой домашний каталог `basename ~/`." # Можно указать просто ~.
echo "Каталог моего домашнего каталога `dirname ~/`." # Можно указать просто ~.
exit 0

split
Утилита разбивает файл на несколько частей. Обычно используется для разбиения больших файлов, чтобы их можно было записать на дискеты или передать по электронной почте по частям.
sum,cksum,md5sum
Эти утилиты предназначены для вычисления контрольных сумм. Контрольная сумма — это некоторое число, вычисляемое исходя из содержимого файла, и служит для контроля целостности информации в файле. Сценарий может выполнять проверку контрольных сумм для того, чтобы убедиться, что файл не был изменен или повреждён. Для большей безопасности, рекомендуется использовать 128-битную сумму, генерируемую утилитой md5sum (messagedigest checksum).

bash$ cksum /boot/vmlinuz
1670054224 804083 /boot/vmlinuz
bash$ md5sum /boot/vmlinuz
0f43eccea8f09e0a0b2b5cf1dcf333ba /boot/vmlinuz

Обратите внимание: утилита cksum выводит контрольную сумму и размер файла в байтах.

Пример: Проверка целостности файла.

#!/bin/bash
# file-integrity.sh: Проверка целостности файлов в заданном каталоге
E_DIR_NOMATCH=70
E_BAD_DBFILE=71
dbfile=File_record.md5
# Файл для хранения контрольных сумм.
set_up_database ()
{
 echo ""$directory"" > "$dbfile"
 # Записать название каталога в первую строку файла.
 md5sum "$directory"/* >> "$dbfile"
 # Записать контрольные суммы md5 и имена файлов.
}
check_database ()
{
 local n=0
 local filename
 local checksum
 # ------------------------------------------- #
 # Возможно эта проверка и не нужна,
 # но лучше перестраховаться сейчас, чем жалеть об этом потом.
 if [ ! -r "$dbfile" ]
 then
  echo "Не могу прочитать файл с контрольными суммами!"
  exit $E_BAD_DBFILE
 fi
 # ------------------------------------------- #
 while read record[n]
 do
  directory_checked="${record[0]}"
  if [ "$directory_checked" != "$directory" ]
  then
   echo "Имя каталога не совпадает с записаным в файле!"
   # Попытка использовать файл контрольных сумм для другого каталога.
   exit $E_DIR_NOMATCH
  fi
  if [ "$n" -gt 0 ] # Не имя каталога.
  then
   filename[n]=$( echo ${record[$n]} | awk '{ print $2 }' )
   # md5sum записывает в обратном порядке,
   # сначала контрольную сумму, затем имя файла.
   checksum[n]=$( md5sum "${filename[n]}" )
   if [ "${record[n]}" = "${checksum[n]}" ]
   then
    echo "Файл ${filename[n]} не был изменен."
   else
    echo "ОШИБКА КОНТРОЛЬНОЙ СУММЫ для файла ${filename[n]}!"
    # Файл был изменен со времени последней проверки.
   fi
  fi
  let "n+=1"
done <"$dbfile" # Чтение контрольных сумм из файла.
}
# =================================================== #
# main ()
if [ -z "$1" ]
then
 directory="$PWD" # Если каталог не задан,
else              # то используется текущий каталог.
 directory="$1"
fi
clear
# Очистка экрана.
# ------------------------------------------------------------------ #
if [ ! -r "$dbfile" ] # Необходимо создать файл с контрольными суммами?
then
 echo "Создание файла с контрольными суммами, \""$directory"/"$dbfile"\"."; echo
 set_up_database
fi
# ------------------------------------------------------------------ #
check_database # Выполнить проверку.
echo
# Вывод этого сценария можно перенаправить в файл,
# это особенно полезно при проверке большого количества файлов.
# Более строгая проверка целостности файлов,
# может быть выполнена с помощью пакета "Tripwire",
# http://sourceforge.net/projects/tripwire/.
exit 0

shred
Надёжное, с точки зрения безопасности, стирание файла, посредством предварительной, многократной записи в файл случайной информации, перед тем как удалить его. Является составной частью пакета GNU fileutils. Имеется ряд технологий, с помощью которых всё-таки возможно восстановить файлы, удаленные утилитой shred.
Кодирование и шифрование
uuencode
Эта утилита используется для кодирования двоичных файлов в символы ASCII, после такого кодирования файлы могут, с достаточной степенью безопасности, передаваться по сети, вкладываться в электронные письма и т.п..
uudecode
Утилита декодирования файлов, прошедших обработку утилитой uuencode.

Пример: Декодирование файлов.

#!/bin/bash
lines=35 # 35 строк для заголовка (более чем достаточно).
for File in * # Обход всех файлов в текущем каталоге...
do
 search1=`head -$lines $File | grep begin | wc -w`
 search2=`tail -$lines $File | grep end | wc -w`
 # Закодированные файлы начинаются со слова "begin",
 # и заканчиваются словом "end".
 if [ "$search1" -gt 0 ]
 then
  if [ "$search2" -gt 0 ]
  then
   echo "декодируется файл - $File -"
   uudecode $File
  fi
 fi
done
# Обратите внимание: если передать сценарию самого себя, для декодирования,
# то это введёт его в заблуждение поскольку в тексте сценария встречаются слова "begin" и "end".
exit 0

При декодировании и выводе длинных текстовых сообщений из новостных групп Usenet, очень нелишним будет передать текст, по конвейеру, команде fold -s.
mimencode,mmencode
Утилиты mimencode и mmencode предназначены для обработки закодированных мультимедийных вложений в электронные письма. Хотя почтовые программы (такие как pine или kmail) имеют возможность автоматической обработки таких вложений, тем не менее эти утилиты позволяют обрабатывать вложения вручную, из командной строки или в пакетном режиме, из сценария на языке командной оболочки.
crypt
Одно время, это была стандартная, для Unix, утилита шифрования файлов. Политически мотивированные, правительственные постановления ряда стран, напрямую запрещают экспорт программного обеспечения для шифрования, что, в результате, привело практически к полному исчезновению crypt из большинства Unix-систем (в том числе и Linux). К счастью,программистами было разработано множество вполне приличных альтернатив, и среди них cruft.
Прочее
mktemp
Создаёт временный файл с "уникальным" именем.

PREFIX=filename
tempfile=`mktemp $PREFIX.XXXXXX`
#                        ^^^^^^ Необходимо по меньшей мере 6 заполнителейecho "имя временного файла = $tempfile"
echo "имя временного файла = $tempfile"
# имя временного файла = filename.QA2ZpY или нечто подобное...

make
Утилита для компиляции и сборки программ. Но может использоваться для выполнения любых других операций, основанных на анализе наличия изменений в исходных файлах. Команда make использует в своей работе Makefile, который содержит перечень зависимостей и операций, которые необходимо выполнить для удовлетворения этих зависимостей.
install
Своего рода — утилита копирования файлов, похожа на cp, но дополнительно позволяет изменять права доступа и атрибуты копируемых файлов. Напрямую эта команда практически не используется, чаще всего она встречается в Makefile (в разделе make install :). Она может использоваться в сценариях установки ПО.
dos2unix
Автор утилиты — Benjamin Lin со-товарищи. Предназначена для преобразования текстовых файлов из формата DOS (в котором строки завершаются комбинацией символов CR-LF) в формат Unix (в котором строки завершаются одним символом LF) и обратно.
ptx
Команда ptx [targetfile] выводит a упорядоченный предметный указатель для targetfile, который можно обработать, по мере необходимости, какой-либо утилитой форматирования, в конвейере.
more,less
Команды постраничного просмотра текстовых файлов или потоков на stdout. Могут использоваться в сценариях в качестве фильтров.

Команды для работы с сетью

Команды, описываемые в этом разделе, могут найти применение при исследовании и анализе процессов передачи данных по сети, а также могут использоваться в борьбе со спамерами. Информация и статистика
host
Возвращает информацию об узле Интернета, по заданному имени или IP адресу, выполняя поиск с помощью службы DNS.

bash$ host surfacemail.com
surfacemail.com. has address 202.92.42.236

ipcalc
Выводит информацию о заданном узле сети. С ключом -h, ipcalc выполняет поиск имени хоста в DNS, по заданному IP адресу.

bash$ ipcalc -h 202.92.42.236
HOSTNAME=surfacemail.com

nslookup
Выполняет "поиск имени узла" Интернета по заданному IP адресу. По сути, эквивалентна командам ipcalc -h и dig -x. Команда может исполняться как в интерактивном, так и в неинтерактивном режиме, т.е. в пределах сценария.

bash$ nslookup -sil 66.97.104.180
nslookup kuhleersparnis.ch
Server: 135.116.137.2
Address: 135.116.137.2#53
Non-authoritative answer:
Name: kuhleersparnis.ch

dig
Подобно команде nslookup, выполняет "поиск имени узла" в Интернете. Сравните вывод команды dig -x с выводом команд ipcalc -h и nslookup.

bash$ dig -x 81.9.6.2
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 11649
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0
;; QUESTION SECTION:
;2.6.9.81.in-addr.arpa.             IN            PTR
 
;; AUTHORITY SECTION:
6.9.81.in-addr.arpa.         3600   IN            SOA ns.eltel.net. noc.eltel.net.
2002031705        900    600   86400 3600
 
;; Query time: 537 msec
;; SERVER: 135.116.137.2#53(135.116.137.2)
;; WHEN: Wed Jun 26 08:35:24 2002
;; MSG SIZE rcvd: 91

traceroute
Утилита предназначена для исследования топологии сети посредством передачи ICMP пакетов удалённому узлу. Эта программа может работать в LAN, WAN и в Интернет. Удаленный узел может быть указан как по имени, так и по IP адресу. Вывод команды traceroute может быть передан по конвейеру утилитам grep или sed, для дальнейшего анализа.

bash$ traceroute 81.9.6.2
traceroute to 81.9.6.2 (81.9.6.2), 30 hops max, 38 byte packets
1 tc43.xjbnnbrb.com (136.30.178.8) 191.303 ms 179.400 ms 179.767 ms
2 or0.xjbnnbrb.com (136.30.178.1) 179.536 ms 179.534 ms 169.685 ms
3 192.168.11.101 (192.168.11.101) 189.471 ms 189.556 ms *
...

ping
Выполняет передачу пакета "ICMP ECHO_REQUEST" другой системе в сети. Чаще всего служит в качестве инструмента диагностики соединений, должна использоваться с большой осторожностью. В случае успеха, ping возвращает код завершения 0, поэтому команда ping может использоваться в условных операторах.

bash$ ping localhost
PING localhost.localdomain (127.0.0.1) from 127.0.0.1 : 56(84) bytes of data.
Warning: time of day goes back, taking countermeasures.
64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=0 ttl=255 time=709 usec
64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=1 ttl=255 time=286 usec
--- localhost.localdomain ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/mdev = 0.286/0.497/0.709/0.212 ms

whois
Выполняет поиск в DNS (Domain Name System). Ключом -h можно указать какой из whois серверов будет запрошен.
finger
Возвращает информацию о пользователях в сети. По желанию, эта команда может выводить содержимое файлов ~/.plan, ~/.project и ~/.forward, указанного пользователя.

bash$ finger
Login Name Tty Idle Login Time Office Office Phone
bozo Bozo Bozeman tty1 8 Jun 25 16:59
bozo Bozo Bozeman ttyp0 Jun 25 16:59
bozo Bozo Bozeman ttyp1 Jun 25 17:07
 
bash$ finger bozo
Login: bozo Name: Bozo Bozeman
Directory: /home/bozo Shell: /bin/bash
Office: 2355 Clown St., 543-1234
On since Fri Aug 31 20:13 (MST) on tty1 1 hour 38 minutes idle
On since Fri Aug 31 20:13 (MST) on pts/0 12 seconds idle
On since Fri Aug 31 20:13 (MST) on pts/1
On since Fri Aug 31 20:31 (MST) on pts/2 1 hour 16 minutes idle
No mail.
No Plan.

По соображениям безопасности, в большинстве сетей служба finger, и соответствующий демон,отключена.
chfn
Изменяет некоторые сведения о пользователе, такие как: полное имя, номер кабинета, телефон рабочий и домашний. Которые обычно выводятся командой finger.
vrfy
Проверка адреса электронной почты.
Доступ к удалённым системам
sx,rx
Команды sx и rx служат для приёма/передачи файлов на/из удалённый узел в сети, по протоколу xmodem. Входят в состав пакета minicom.
sz,rz
Команды sz и rz служат для приёма/передачи файлов на/из удаленный узел в сети, по протоколу zmodem. Протокол zmodem имеет некоторые преимущества перед протоколом xmodem, в качестве такого преимущества можно назвать более высокую скорость передачи и возможность возобновления передачи, в случае её разрыва. Входят в состав пакета minicom.
ftp
Под этим именем подразумевается утилита и протокол передачи файлов. Сеансы ftp могут устанавливаться из сценариев.
uucp
Unix to Unix copy. Это коммуникационный пакет для передачи файлов между Unix серверами. Сценарий на языке командной оболочки — один из самых эффективных способов автоматизации такого обмена. Похоже, что с появлением Интернет и электронной почты, uucp постепенно уходит в небытие, однако, она с успехом может использоваться в изолированных, не имеющих выхода в Интернет, сетях.
cu
Call Up — выполняет соединение с удалённой системой, как простой терминал. Эта команда является частью пакета uucp и, своего рода, упрощенным вариантом команды telnet.
telnet
Утилита и протокол для подключения к удалённой системе. Протокол telnet небезопасен по своей природе, поэтому следует воздерживаться от его использования.
wget
wget — неинтерактивная утилита для скачивания файлов с Web или ftp сайтов.

wget -p http://www.xyz23.com/file01.html
wget -r ftp://ftp.xyz24.net/~bozo/project_files/ -o $SAVEFILE

lynx
lynx — Web-браузер, внутри сценариев (с ключом -dump) может использоваться для скачивания файлов с Web или ftp сайтов, в неинтерактивном режиме.

lynx -dump http://www.xyz23.com/file01.html >$SAVEFILE

rlogin
Remote login — инициирует сессию с удалённой системой. Эта команда небезопасна, вместо неё лучше использовать ssh.
rsh
Remote shell — исполняет команду на удалённой системе. Эта команда небезопасна, вместо неё лучше использовать ssh.
rcp
Remote copy — копирование файлов между двумя машинами через сеть. Подобно прочим r*утилитам, команда rcp небезопасна и потому, использовать её в сценариях нежелательно. В качестве замены можно порекомендовать ssh или expect.
ssh
Secure shell — устанавливает сеанс связи и выполняет команды на удалённой системе. Выступает в качестве защищённой замены для telnet, rlogin, rcp и rsh. Использует идентификацию, аутентификацию и шифрование информации, передаваемой через сеть. Подробности вы найдете в man ssh.
Локальная сеть
write
Эта утилита позволяет передать текст сообщения на другой терминал (console или xterm). Разрешить или запретить доступ к терминалу можно с помощью команды mesg. Поскольку команда write работает в интерактивном режиме, то, как правило, она не употребляется в сценариях.
MAIL
mail
Чтение или передача электронной почты. Этот почтовый клиент командной строки с успехом может использоваться в сценариях.

Пример: Сценарий, отправляющий себя самого по электронной почте.

#!/bin/bash
# self-mailer.sh: Сценарий отправляет себя самого по электронной почте
adr=${1:-`whoami`} # Если пользователь не указан, то — себе самому.
# Вызов 'self-mailer.sh wiseguy@superdupergenius.com'
# приведёт к передаче электронного письма по указанному адресу.
# Вызов 'self-mailer.sh' (без аргументов) — отправит письмо
# пользователю, запустившему сценарий, например, bozo@localhost.localdomain.
#
# Дополнительно о конструкции ${parameter:-default},
# см. раздел "Подстановка параметров"
# в главе "К вопросу о переменных".
# ============================================================================
cat $0 | mail -s "Сценарий \"`basename $0`\" отправил себя сам." "$adr"
# ============================================================================
# --------------------------------------------
# Поздравляю!
# Этот сценарий запустила какая-то "редиска",
# и заставила отправить этот текст к Вам.
# Очевидно кто-то не знает куда девать свое время.
# --------------------------------------------
echo "`date`, сценарий \"`basename $0`\" отправлен "$adr"."
exit 0

mailto
Команда mailto, похожа на mail, она также отправляет сообщения по электронной почте. Однако, кроме этого, mailto позволяет отправлять MIME (multimedia) сообщения.
vacation
Эта утилита предназначена для автоматической передачи ответов на электронные письма, например для того, чтобы уведомить отправителя о том, что получатель временно отсутствует. Работает совместно с sendmail и не может использоваться для передачи сообщений через коммутируемые линии (по модему).

Команды управления терминалом

Команды, имеющие отношение к консоли или терминалу
tput
Инициализация терминала или выполнение запроса к базе данных терминалов terminfo. С помощью tput можно выполнять различные операции. tput clear — эквивалентно команде clear. tput reset — эквивалентно команде reset. tput sgr0 — так же сбрасывает настройки терминал, но без очистки экрана.

bash$ tput longname
xterm terminal emulator (XFree86 4.0 Window System)

Команда tput cup X Y перемещает курсор в координаты (X,Y). Обычно этой команде предшествует clear, очищающая экран.
Обратите внимание: stty предлагает более широкий диапазон возможностей.
infocmp
Cравнение или печать информации о характеристиках терминалов, хранящейся в базе данных terminfo.

bash$ infocmp
# Reconstructed via infocmp from file:
/usr/share/terminfo/r/rxvt
rxvt|rxvt terminal emulator (X Window System),
am, bce, eo, km, mir, msgr, xenl, xon,
colors#8, cols#80, it#8, lines#24, pairs#64,
acsc=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
bel=^G, blink=\E[5m, bold=\E[1m,
civis=\E[?25l,
clear=\E[H\E[2J, cnorm=\E[?25h, cr=^M,
...

reset
Сбрасывает настройки терминала и очищает экран. Как и в случае команды clear, курсор и приглашение к вводу (prompt) выводятся в верхнем левом углу терминала.
clear
Команда clear просто очищает экран терминала или окно xterm. Курсор и приглашение к вводу (prompt) выводятся в верхнем левом углу терминала. Эта команда может запускаться как из командной строки, так и из сценария.
script
Эта утилита позволяет сохранять в файле все символы, введённые пользователем c клавиатуры(вывод тоже). Получая, фактически, подробнейший синхронный протокол сессии.

Команды выполнения математических операций

factor
Разложение целого числа на простые множители.

bash$ factor 27417
27417: 3 13 19 37

bc
Bash не в состоянии выполнять действия над числами с плавающей запятой и не содержит многих важных математических функций. К счастью существует bc. Универсальная, выполняющая вычисления с произвольной точностью, утилита bc обладает некоторыми возможностями, характерными для языков программирования. Синтаксис bc немного напоминает язык C. Поскольку это утилита Unix, то она может достаточно широко использоваться в сценариях на языке командной оболочки, в том числе и в конвейерной обработке данных. Ниже приводится простой шаблон работы с утилитой bc в сценарии. Здесь используется приём подстановки команд.

variable=$(echo "OPTIONS; OPERATIONS" | bc)

Пример: Ежемесячные выплаты по займу.

#!/bin/bash
# monthlypmt.sh: Расчёт ежемесячных выплат по займу.
# Это измененный вариант пакета "mcalc" (mortgage calculator),
# написанного Jeff Schmidt и Mendel Cooper (ваш покорный слуга).
# http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz
echo
echo "Введите сумму займа, процентную ставку и срок займа,"
echo "для расчета суммы ежемесячных выплат."
bottom=1.0
echo
echo -n "Сумма займа (без запятых — с точностью до доллара) "
read principal
echo -n "Процентная ставка (процент) " # Если 12%, то нужно вводить "12", а не ".12".
read interest_r
echo -n "Срок займа (месяцев) "
read term
interest_r=$(echo "scale=9; $interest_r/100.0" | bc) # Здесь "scale" — точность вычислений.
interest_rate=$(echo "scale=9; $interest_r/12 + 1.0" | bc)
top=$(echo "scale=9; $principal*$interest_rate^$term" | bc)
echo; echo "Прошу подождать. Вычисления потребуют некоторого времени."
let "months = $term - 1"
# ====================================================================
for ((x=$months; x > 0; x--))
do
 bot=$(echo "scale=9; $interest_rate^$x" | bc)
 bottom=$(echo "scale=9; $bottom+$bot" | bc)
# bottom = $(($bottom + $bot"))
done
# --------------------------------------------------------------------
# Rick Boivie предложил более эффективную реализацию
# цикла вычислений, который дает выигрыш по времени на 2/3.
# for ((x=1; x <= $months; x++))
# do
#  bottom=$(echo "scale=9; $bottom * $interest_rate + 1" | bc)
# done
# А затем нашёл ещё более эффективную альтернативу,
# которая выполняется в 20 раз быстрее !!!
# bottom=`{
# echo "scale=9; bottom=$bottom; interest_rate=$interest_rate"
# for ((x=1; x <= $months; x++))
# do
#  echo 'bottom = bottom * interest_rate + 1'
# done
# echo 'bottom'
# } | bc` # Внедрить цикл 'for' в конструкцию подстановки команд.
# ====================================================================
# let "payment = $top/$bottom"
payment=$(echo "scale=2; $top/$bottom" | bc)
# Два знака после запятой, чтобы показать доллары и центы.
 
echo
echo "ежемесячные выплаты = \$$payment" # Вывести знак "доллара" перед числом.
echo
 
exit 0
# Упражнения:
# 1) Добавьте возможность ввода суммы с точностью до цента.
# 2) Добавьте возможность ввода процентной ставки как в виде процентов, так и в виде десятичного числа — доли целого.
# 3) Если вы действительно честолюбивы, добавьте в сценарий вывод полной таблицы помесячных выплат.

Пример: Перевод чисел из одной системы счисления в другую.

##########################################################################
# Shellscript: base.sh - вывод чисел в разных системах счисления (Bourne Shell)
# Author : Heiner Steven (heiner.steven@odn.de)
# Date : 07-03-95
# Category : Desktop
# $Id: base.sh,v 1.2 2000/02/06 19:55:35 heiner Exp $
##########################################################################
# Description
#
# Changes
# 21-03-95 stv исправлена ошибка, возникающая при вводе числа 0xb (0.2)
##########################################################################
# ==> Используется в данном документе с разрешения автора.
# ==> Комментарии добавлены автором документа.
NOARGS=65
PN=`basename "$0"` # Имя программы
VER=`echo '$Revision: 1.2 $' | cut -d' ' -f2` # ==> VER=1.2
Usage () {
 echo "$PN - вывод чисел в различных системах счисления, $VER (stv '95)
Порядок использования: $PN [number ...]
Если число не задано, то производится ввод со stdin.
Число может быть:
двоичное — должно начинаться с комбинации символов 0b (например 0b1100)
восьмеричное — должно начинаться с 0 (например 014)
шестнадцатиричное — должно начинаться с комбинации символов 0x (например 0xc)
десятичное — в любом другом случае (например 12)" >&2
exit $NOARGS
} # ==> Функция вывода сообщения о порядке использования.
Msg () {
for i # ==> [список] параметров опущен.
do 
 echo "$PN: $i" >&2
done
}
Fatal () { Msg "$@"; exit 66; }
PrintBases () {
# Определение системы счисления
for i # ==> [список] параметров опущен...
do # ==> поэтому работает с аргументами командной строки.
 case "$i" in
  0b*) ibase=2;; # двоичная
  0x*|[a-f]*|[A-F]*) ibase=16;;    # шестнадцатиричная
  0*) ibase=8;;                    # восьмеричная
  [1-9]*) ibase=10;;               # десятичная
  *)
  Msg "Ошибка в числе $i - число проигнорировано"
  continue;;
 esac
# Удалить префикс и преобразовать шестнадцатиричные цифры в верхний регистр (этого требует bc)
  number=`echo "$i" | sed -e 's:^0[bBxX]::' | tr '[a-f]' '[A-F]'`
# ==> вместо "/", здесь используется символ ":" как разделитель для sed.
# Преобразование в десятичную систему счисления 
 dec=`echo "ibase=$ibase; $number" | bc` # ==> 'bc' используется как калькулятор.
 case "$dec" in
  [0-9]*) ;;                 # всё в порядке
  *) continue;;              # ошибка: игнорировать
 esac
# Напечатать все преобразования в одну строку.
# ==> 'вложенный документ' — список команд для 'bc'.
 echo `bc <<!
 obase=16; "hex="; $dec
 obase=10; "dec="; $dec
 obase=8; "oct="; $dec
 obase=2; "bin="; $dec
!
` | sed -e 's: : :g'
done
}
while [ $# -gt 0 ]
do
 case "$1" in
  --) shift; break;;
  -h) Usage;; # ==> Вывод справочного сообщения.
  -*) Usage;;
  *) break;; # первое число
 esac # ==> Хорошо бы расширить анализ вводимых символов.
 shift
done
if [ $# -gt 0 ]
then
PrintBases "$@"
else # чтение со stdin
 while read line
 do
  PrintBases $line
 done
fi

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

variable=`bc << LIMIT_STRING
options
statements
operations
LIMIT_STRING`

...или...

variable=$(bc << LIMIT_STRING
options
statements
operations
LIMIT_STRING)

Пример: Пример взаимодействия bc со "встроенным документом".

#!/bin/bash
# Комбинирование 'bc' с
# 'вложенным документом'.
var1=`bc << EOF
18.33 * 19.78
EOF`
 
echo $var1 # 362.56
# запись $( ... ) тоже работает.
v1=23.53
v2=17.881
v3=83.501
v4=171.63
 
var2=$(bc << EOF
scale = 4
a = ( $v1 + $v2 )
b = ( $v3 * $v4 )
a * b + 15.35
EOF
)
echo $var2 # 593487.8452
var3=$(bc -l << EOF
scale = 9
s ( 1.7 )
EOF
)
# Возвращается значение синуса от 1.7 радиана.
# Ключом "-l" вызывается математическая библиотека 'bc'.
echo $var3 # .991664810
# Попробуем функции...
hyp= # Объявление глобальной переменной.
hypotenuse () # Расчёт гипотенузы прямоугольного треугольника.
{
hyp=$(bc -l << EOF
scale = 9
sqrt ( $1 * $1 + $2 * $2 )
EOF
)
# К сожалению, функции Bash не могут возвращать числа с плавающей запятой.
}
hypotenuse 3.68 7.31
echo "гипотенуза = $hyp" # 8.184039344
exit 0

Пример: Вычисление числа "пи".

#!/bin/bash
# cannon.sh: Аппроксимация числа "пи".
# Это очень простой вариант реализации метода "Monte Carlo",
# математическое моделирование событий реальной жизни,
# для эмуляции случайного события используются псевдослучайные числа.
 
# Допустим, что мы располагаем картой квадратного участка поверхности со стороной квадрата 10000 единиц.
# На этом участке, в центре, находится совершенно круглое озеро, с диаметром в 10000 единиц.
# Т.е. озеро покрывает почти всю карту, кроме её углов.
# (Фактически — это квадрат со вписанным кругом.)
#
# Пусть по этому участку ведется стрельба железными ядрами из древней пушки
# Все ядра падают где-то в пределах данного участка, т.е. либо в озеро, либо на сушу, по углам участка.
# Поскольку озеро покрывает большую часть участка, то большинство ядер будет падать в воду.
# Незначительная часть ядер будет падать на твердую почву.
#
# Если произвести достаточно большое число неприцельных выстрелов по данному участку,
# то отношение попаданий в воду к общему числу выстрелов будет примерно равно#+ значению PI/4.
#
# По той простой причине, что стрельба фактически ведётся только по правому верхнему квадранту карты.
# (Предыдущее описание было несколько упрощено.)
#
# Теоретически, чем больше будет произведено выстрелов, тем точнее будет результат.
# Однако, сценарий на языке командной оболочки, в отличие от других языков программирования,
# в которых доступны операции с плавающей запятой, имеет некоторые ограничения.
# К сожалению, это делает вычисления менее точными.
 
DIMENSION=10000 # Длина стороны квадратного участка поверхности.
                # Он же — верхний предел для генератора случайных чисел.
MAXSHOTS=1000   # Количество выстрелов.
                # 10000 выстрелов (или больше) даст лучший результат, но потребует значительного количества времени.
PMULTIPLIER=4.0 # Масштабирующий коэффициент.
 
get_random ()
{
SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
RANDOM=$SEED    # Из примера "seeding-random.sh"
let "rnum = $RANDOM % $DIMENSION" # Число не более чем 10000.
echo $rnum
}
distance= # Объявление глобальной переменной.
hypotenuse () # Расчет гипотенузы прямоугольного треугольника.
{ # Из примера "alt-bc.sh".
distance=$(bc -l << EOF
scale = 0
sqrt ( $1 * $1 + $2 * $2 )
EOF
)
# Установка "scale" в ноль приводит к округлению результата "вниз",
# это и есть то самое ограничение, накладываемое командной оболочкой.
# Что, к сожалению, снижает точность аппроксимации.
}
 
# main() {
# Инициализация переменных.
shots=0
splashes=0
thuds=0
Pi=0
while [ "$shots" -lt "$MAXSHOTS" ] # Главный цикл.
do
 xCoord=$(get_random) # Получить случайные координаты X и Y.
 yCoord=$(get_random)
 hypotenuse $xCoord $yCoord # Гипотенуза = расстоянию.
 ((shots++))
 
 printf "#%4d " $shots
 printf "Xc = %4d " $xCoord
 printf "Yc = %4d " $yCoord
 printf "Distance = %5d " $distance # Расстояние от центра озера, с координатами (0,0).
 
 if [ "$distance" -le "$DIMENSION" ]
 then
  echo -n "ШЛЁП! " # попадание в озеро
  ((splashes++))
 else
  echo -n "БУХ! " # попадание на твердую почву
  ((thuds++))
 fi
 Pi=$(echo "scale=9; $PMULTIPLIER*$splashes/$shots" | bc)
 # Умножение на коэффициент 4.0.
 echo -n "PI ~ $Pi"
 echo
done
echo
echo "После $shots выстрела, примерное значение числа \"пи\" равно $Pi."
# Имеет тенденцию к завышению...
# Вероятно из-за ошибок округления и несовершенства генератора случайных чисел.
echo
# 
}
exit 0
# Самое время задуматься над тем, является ли сценарий удобным средством
# для выполнения большого количества столь сложных вычислений.
#
# Тем не менее, этот пример может расцениваться как
# 1) Доказательство возможностей языка командной оболочки.
# 2) Прототип для "обкатки" алгоритма перед тем как перенести
# его на высокоуровневые языки программирования компилирующего типа.

dc
Утилита dc (desk calculator) — это калькулятор, использующий "Обратную Польскую Нотацию", и ориентированный на работу со стеком. Многие стараются избегать использования dc, из-за непривычной формы записи операндов и операций. Однако, dc имеет и своих сторонников.

Пример: Преобразование чисел из десятичной в шестнадцатиричную систему счисления.

#!/bin/bash
# hexconvert.sh: Преобразование чисел из десятичной в шестнадцатиричную систему счисления.
BASE=16 # Шестнадцатиричная.
if [ -z "$1" ]
then
 echo "Порядок использования: $0 number"
 exit $E_NOARGS
 # Необходим аргумент командной строки.
fi
# Упражнение: добавьте проверку корректности аргумента.
hexcvt ()
{
if [ -z "$1" ]
then
echo 0
return # "Return" 0, если функции не был передан аргумент.
fi
echo ""$1" "$BASE" o p" | dc
# "o" устанавливает основание системы счисления для вывода.
# "p" выводит число, находящееся на вершине стека.
# См. 'man dc'.
return
}
hexcvt "$1"
exit 0

Изучение страниц info dc позволит детальнее разобраться с утилитой. Однако, отряд "гуру", которые могут похвастать своим знанием этой мощной, но весьма запутанной утилиты, весьма немногочислен.

Пример: Разложение числа на простые множители.

#!/bin/bash
# factr.sh: Разложение числа на простые множители
MIN=2 # Не работает с числами меньше 2.
E_NOARGS=65
E_TOOSMALL=66
 
if [ -z $1 ]
then
 echo "Порядок использования: $0 number"
 exit $E_NOARGS
fi
if [ "$1" -lt "$MIN" ]
then 
 echo "Исходное число должно быть больше или равно $MIN."
exit $E_TOOSMALL
fi
# Упражнение: Добавьте проверку типа числа (не целые числа должны отвергаться).
echo "Простые множители для числа $1:"
# ---------------------------------------------------------------------------------
echo "$1[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr[dli%0=1lrli2+dsi!>.]ds.xd1<2" | dc
# ---------------------------------------------------------------------------------
# Автор вышеприведенной строки: Michel Charpentier <charpov@cs.unh.edu>.
# Используется с его разрешения (спасибо).
exit 0

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

Пример: Расчёт гипотенузы прямоугольного треугольника.

#!/bin/bash
# hypotenuse.sh: Возвращает "гипотенузу" прямоугольного треугольника.
# ( корень квадратный от суммы квадратов катетов)
 
ARGS=2 # В сценарий необходимо передать два катета.
E_BADARGS=65 # Ошибка в аргументах.
if [ $# -ne "$ARGS" ] # Проверка количества аргументов.
then
 echo -e "\e[32mПорядок использования\e[0m: \e[31m`basename $0`\e[0m \e[34mкатет_1\e[0m \e[33mкатет_2\e[0m"
 exit $E_BADARGS
fi
AWKSCRIPT=' { printf( "%3.7f\n", sqrt($1*$1 + $2*$2) ) } '
# команды и параметры, передаваемые в awk
echo -n "Гипотенуза прямоугольного треугольника, с катетами $1 и $2 = "
echo $1 $2 | awk "$AWKSCRIPT"
exit 0

Прочие команды

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

bash$ seq 5
1
2
3
4
5
bash$ seq -s : 5
1:2:3:4:5

Обе утилиты, и jot, и seq, очень удобно использовать для генерации списка аргументов в цикле for.

Пример: Использование seq для генерации списка аргументов цикла for.

#!/bin/bash
# Утилита "seq"
echo
for a in `seq 80` # или так: for a in $( seq 80 )
# То же самое, что и for a in 1 2 3 4 5 ... 80 (но как экономит время и силы!).
# Можно использовать и 'jot' (если эта утилита имеется в системе).
do
 echo -n "$a "
done # 1 2 3 4 5 ... 80
# Пример использования вывода команды для генерации
# [списка] аргументов цикла "for".
echo; echo
COUNT=80 # Да, 'seq' допускает указание переменных в качестве параметра.
for a in `seq $COUNT` # или так: for a in $( seq $COUNT )
do
 echo -n "$a "
done # 1 2 3 4 5 ... 80
echo; echo
BEGIN=75
END=80
for a in `seq $BEGIN $END`
# Если "seq" передаются два аргумента, то первый означает начальное число последовательности, второй — последнее,
do
 echo -n "$a "
done # 75 76 77 78 79 80
echo; echo
BEGIN=45
INTERVAL=5
END=80
for a in `seq $BEGIN $INTERVAL $END`
# Если "seq" передаётся три аргумента, то первый аргумент — начальное число в последовательности,
# второй — шаг последовательности, и третий — последнее число в последовательности.
do
 echo -n "$a "
done # 45 50 55 60 65 70 75 80
echo; echo
exit 0

getopt
Команда getopt служит для разбора командной строки, выделяя из неё ключи — символы, с предшествующим символом дефиса. В этой утилите имеется, встроенный в Bash, аналог — getopts, более мощная и универсальная команда. Тем не менее, команда getopt, с ключом -l, позволяет производить разбор "длинных" ключей и допускает переупорядочивание ключей.

Пример: Использование getopt для разбора аргументов командной строки.

#!/bin/bash
# ex33a.sh
 
# Попробуйте следующие варианты вызова этого сценария.
# sh ex33a -a
# sh ex33a -abc
# sh ex33a -a -b -c
# sh ex33a -d
# sh ex33a -dXYZ
# sh ex33a -d XYZ
# sh ex33a -abcd
# sh ex33a -abcdZ
# sh ex33a -z
# sh ex33a a
# Объясните полученные результаты.
E_OPTERR=65
if [ "$#" -eq 0 ]
then # Необходим по меньшей мере один аргумент.
 echo "Порядок использования: $0 -[options a,b,c]"
 exit $E_OPTERR
fi
set -- `getopt "abcd:" "$@"`
# Запись аргументов командной строки в позиционные параметры.
# Что произойдёт, если вместо "$@" указать "$*"?
while [ ! -z "$1" ]
do
 case "$1" in
  -a) echo "Опция \"a\"";;
  -b) echo "Опция \"b\"";;
  -c) echo "Опция \"c\"";;
  -d) echo "Опция \"d\" $2";;
  *) break;;
 esac
 shift
done 
# Вместо 'getopt' лучше использовать встроенную команду 'getopts',
exit 0

run-parts
Команда run-parts запускает на исполнение все сценарии, в порядке возрастания имён файлов-сценариев, в заданном каталоге. Естественно, файлы сценариев должны иметь права на исполнение. Демон cron вызывает run-parts для запуска сценариев из каталогов /etc/cron.*.
yes
По-умолчанию, команда yes выводит на stdout непрерывную последовательность символов y, разделённых символами перевода строки. Исполнение команды можно прервать комбинацией клавиш Ctrl-c. Команду yes можно заставить выводить иную последовательность символов. Теперь самое время задаться вопросом о практической пользе этой команды. Основное применение этой команды состоит в том, что вывод от неё может быть передан, через конвейер, другой команде, ожидающей реакции пользователя. В результате получается, своего рода, слабенькая версия команды expect.
Запускает fsck в неинтерактивном режиме (будьте осторожны!):

yes | fsck /dev/hda1

Имеет тот же эффект, что и rm -rf dirname (будьте осторожны!):

yes | rm -r dirname

Внимание!Передача вывода команды yes по конвейеру потенциально опасным командам, таким как fsck или fdisk может дать нежелательные побочные эффекты.
banner
Печатает на stdout заданную строку символов (не более 10), рисуя каждый символ строки при помощи символа '#'. Вывод от команды может быть перенаправлен на принтер.
printenv
Выводит все переменные окружения текущего пользователя.

bash$ printenv | grep HOME
HOME=/home/bozo

lp
Команды lp и lpr отправляют файлы в очередь печати для вывода на принтер. Названия этих команд произошли от "line printers".

bash$ lp file1.txt или bash lp <file1.txt

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

bash$ pr -options file1.txt | lp

Программы подготовки текста к печати, такие как groff и Ghostscript, так же могут напрямую взаимодействовать с lp.

bash$ groff -Tascii file.tr | lp
bash$ gs -options | lp file.ps

Команда lpq предназначена для просмотра очереди заданий печати, а lprm — для удаления заданий из очереди.
tee
[Unix заимствовал эту идею из водопроводного дела.] Это оператор перенаправления, но с некоторыми особенностями. Подобно водопроводным трубам, tee позволяет "направить поток" данных в несколько файлов и на stdout одновременно, никак не влияя на сами данные. Эта команда может оказаться очень полезной при отладке.

           tee |------> в файл
               |
===============|===============
command--->----|-operator-->---> результат работы команд(ы)
==================
cat listfile* | sort | tee check.file | uniq > result.file

(Здесь, в файл check.file будут записаны данные из всех "listfile*", в отсортированном виде до того, как повторяющиеся строки будут удалены командой uniq.)
mkfifo
Эта, редко встречающаяся, команда создаёт именованный канал — очередь, через который производится обмен данными между процессами. Как правило, один процесс записывает данные в очередь (FIFO), а другой читает данные из очереди.
pathchk
Производит проверку полного имени файла — проверяет, доступны ли на чтение, каталоги в пути к файлу, и не превышает ли длина полного имени файла 255 символов. При несоблюдении одного из условий — возвращает сообщение об ошибке. К сожалению, pathchk не возвращает соответствующего кода ошибки, и потому, в общем-то, бесполезна в сценариях. Вместо неё лучше использовать операторы проверки файлов.
dd
Эта немного непонятная и "страшная" команда ("data duplicator") изначально использовалась для переноса данных на магнитной ленте между микрокомпьютерами с ОС Unix и майнфреймами IBM. Команда dd просто создаёт копию файла (или stdin/stdout), выполняя по пути некоторые преобразования. Один из вариантов: преобразование из ASCII в EBCDIC, dd --help выведет список возможных вариантов преобразований и опций этой мощной утилиты.

# Изучаем 'dd'.
n=3
p=5
input_file=project.txt
output_file=log.txt
dd if=$input_file of=$output_file bs=1 skip=$((n-1)) count=$((p-n+1)) 2> /dev/null       # Извлечёт из $input_file символы с n-го по p-й.
echo -n "hello world" | dd cbs=1 conv=unblock 2> /dev/null
# Выведет "hello world" вертикально.
# Спасибо, S.C.

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

Пример: Захват нажатых клавиш.

#!/bin/bash
# Захват нажатых клавиш.
keypresses=4                 # Количество фиксируемых нажатий.
old_tty_setting=$(stty -g)   # Сохранить настройки терминала.
echo "Нажмите $keypresses клавиши."
stty -icanon -echo           # Запретить канонический режим.
                             # Запретить эхо-вывод.
keys=$(dd bs=1 count=$keypresses 2> /dev/null)
# 'dd' использует stdin, если "if" не задан.
stty "$old_tty_setting"      # Восстановить настройки терминала.
echo "Вы нажали клавиши \"$keys\"."
# Спасибо S.C.
exit 0

Команда dd имеет возможность произвольного доступа к данным в потоке.

echo -n . | dd bs=1 seek=4 of=file conv=notrunc
# Здесь, опция "conv=notrunc" означает, что выходной файл не будет усечен.
# Спасибо, S.C.

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

dd if=kernel-image of=/dev/fd0H1440

Точно так же, dd может скопировать всё содержимое дискеты, даже с неизвестной файловой системой, на жёсткий диск в виде файла-образа.

dd if=/dev/fd0 of=/home/bozo/projects/floppy.img

Ещё одно применение dd — создание временного swap-файла и ram-дисков. Она может создавать даже образы целых разделов жёсткого диска, хотя и не рекомендуется делать это без особой на то необходимости. Многие (которые, вероятно, не знают чем себя занять) постоянно придумывают всё новые и новые области применения команды dd.

Пример: Надёжное удаление файла.

#!/bin/bash
# blotout.sh: Надежно удаляет файл.
# Этот сценарий записывает случайные данные в заданный файл, затем записывает туда нули и наконец удаляет файл.
# После такого удаления даже анализ дисковых секторов не даст ровным счётом ничего.
PASSES=7 # Количество проходов по файлу.
BLOCKSIZE=1 # операции ввода/вывода в/из /dev/urandom требуют указания размера блока, иначе вы не получите желаемого результата.
E_BADARGS=70
E_NOT_FOUND=71
E_CHANGED_MIND=72
if [ -z "$1" ] # Имя файла не указано.
then
 echo "Порядок использования: `basename $0` filename"
 exit $E_BADARGS
fi
file=$1
if [ ! -e "$file" ]
then
 echo "Файл \"$file\" не найден."
 exit $E_NOT_FOUND
fi
echo; echo -n "Вы совершенно уверены в том, что желаете уничтожить \"$file\" (y/n)? "
read answer
case "$answer" in
 [nN]) echo "Передумали? Операция отменена."
       exit $E_CHANGED_MIND
       ;;
 *) echo "Уничтожается файл \"$file\".";;
esac
flength=$(ls -l "$file" | awk '{print $5}') # Поле с номером 5 — это длина файла.
pass_count=1
echo
while [ "$pass_count" -le "$PASSES" ]
do
 echo "Проход #$pass_count"
 sync # Вытолкнуть буферы.
 dd if=/dev/urandom of=$file bs=$BLOCKSIZE count=$flength # Заполнить файл случайными данными.
 sync # Снова вытолкнуть буферы.
 dd if=/dev/zero of=$file bs=$BLOCKSIZE count=$flength # Заполнить файл нулями.
 sync # Снова вытолкнуть буферы.
 let "pass_count += 1"
 echo
done
rm -f $file # Наконец удалить изрядно "подпорченный" файл.
sync # Вытолкнуть буферы в последний раз.
echo "Файл \"$file\" уничтожен."; echo
 
# Это довольно надёжный, хотя и достаточно медленный способ уничтожения файлов.
# Более эффективно это делает команда "shred", входящая в состав пакета GNU "fileutils".
# Уничтоженный таким образом файл, не сможет быть восстановлен обычными методами.
# Однако...
# эта метода вероятно НЕ сможет противостоять аналитическим службам из СООТВЕТСТВУЮЩИХ ОРГАНОВ.
# Tom Vier разработал пакет "wipe", который более надежно стирает файлы, чем этот простой сценарий.
# http://www.ibiblio.org/pub/Linux/utils/file/wipe-2.0.0.tar.bz2
# Для более глубоко изучения проблемы надёжного удаления файлов, рекомендую обратиться к cnfnmt Peter Gutmann,
# "Secure Deletion of Data From Magnetic and Solid-State Memory".
# http://www.cs.auckland.ac.nz/~pgut001/pubs/secure_del.html
exit 0

od
Команда od (octal dump) производит преобразование ввода (или файла) в один или несколько форматов, в соответствии с указанными опциями. При отсутствии опций используется восьмеричный формат (опция -o). Эта команда полезна при просмотре или обработке файлов с двоичными данными, например /dev/urandom.
hexdump
Выводит дамп двоичных данных из файла в восьмеричном, шестнадцатиричном, десятичном виде или в виде ASCII. Эту команду, с массой оговорок, можно назвать эквивалентом команды od.
objdump
Отображает сведения об исполняемом или объектном файле либо в шестнадцатиричной форме, либо в виде дизассемблерного листинга (с ключом -d).

bash$ objdump -d /bin/ls
/bin/ls: file format elf32-i386
 
Disassembly of section .init:
080490bc <.init>:
80490bc: 55 push %ebp
80490bd: 89 e5 mov %esp,%ebp
. . .

mcookie
Эта команда создает псевдослучайные шестнадцатиричные 128-битные числа, так называемые "magic cookie", обычно используется X-сервером в качестве "сигнатуры" авторизации. В сценариях может использоваться как малоэффективный генератор случайных чисел.

random000=$(mcookie)

Конечно, для тех же целей, сценарий может использовать md5.

# Сценарий вычисляет контрольную сумму для самого себя.
random001=`md5sum $0 | awk '{print $1}'`
# 'awk' удаляет имя файла.

С помощью mcookie можно создавать "уникальные" имена файлов.

Пример: Генератор имён файлов.

#!/bin/bash
# tempfile-name.sh: Генератор имен временных файлов
BASE_STR=`mcookie`                 # 32-символьный (128 бит) magic cookie.
POS=11                             # Произвольная позиция в строке magic cookie.
LEN=5                              # $LEN последовательных символов.
prefix=temp                        # В конце концов это временный ("temp") файл.
suffix=${BASE_STR:POS:LEN}
                                   # Извлечь строку, длиной в 5 символов, начиная с позиции 11.
temp_filename=$prefix.$suffix
                                   # Сборка имени файла.
echo "Имя временного файла = \"$temp_filename\""
# sh tempfile-name.sh
# Имя временного файла = temp.e19ea
exit 0

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

Пример: Преобразование метров в мили.

#!/bin/bash
# unit-conversion.sh
convert_units () # Принимает в качестве входных параметров единицы измерения.
{
cf=$(units "$1" "$2" | sed --silent -e '1p' | awk '{print $2}')
# Удаляет все кроме коэффициентов преобразования.
echo "$cf"
}
Unit1=miles
Unit2=meters
cfactor=`convert_units $Unit1 $Unit2`
quantity=3.73
result=$(echo $quantity*$cfactor | bc)
echo $quantity милях $result метров."
# Что произойдет, если в функцию передать несовместимые единицы измерения, например "acres" (акры) and "miles" (мили)?
exit 0

m4
Не команда, а клад, m4 — это мощный фильтр обработки макроопределений, фактически — целый язык программирования. Изначально создававшаяся как препроцессор для RatFor, m4 оказалась очень полезной и как самостоятельная утилита. Фактически, m4 сочетает в себе функциональные возможности eval, tr, awk, и дополнительно предоставляет обширные возможности по созданию новых макроопределений. В апрельском выпуске, за 2002 год, журнала Linux Journal вы найдёте замечательную статью, описывающую возможности утилиты m4.

Пример: Работа с m4.

#!/bin/bash
# m4.sh: Демонстрация некоторых возможностей макропроцессора m4
# Строки
string=abcdA01
echo "len($string)" | m4                   # 7
echo "substr($string,4)" | m4              # A01
echo "regexp($string,[0-1][0-1],\&Z)" | m4 # 01Z
# Арифметика
echo "incr(22)" | m4                       # 23
echo "eval(99 / 3)" | m4                   # 33
exit 0

doexec
Команда doexec предоставляет возможность передачи произвольного списка аргументов внешней программе. В частности, передавая argv[0] (для сценариев соответствует специальной переменной $0), можно вызвать программу под другим именем, определяя тем самым, её реакцию. Например, пусть в каталоге /usr/local/bin имеется программа с именем "aaa", которая при вызове doexec /usr/local/bin/aaa list выведет список всех файлов в текущем каталоге, имена которых начинаются с символа "a", а при вызове той же самой программы как doexec/usr/local/bin/aaa delete, она удалит эти файлы. Естественно, реакция программы на своё собственное имя должна быть реализована в коде программы, для сценария на языке командной оболочки это может выглядеть примерно так:

case `basename $0` in
 "name1" ) реакция на вызов под именем name1;;
 "name2" ) реакция на вызов под именем name2;;
 "name3" ) реакция на вызов под именем name3;;
 * ) действия по-умолчанию;;
esac

dialog
Утилита dialog предоставляет в распоряжение программиста целый набор инструментов для построения интерактивного интерфейса в сценариях. Более совершенные разновидности команды dialoggdialog, Xdialog и kdialog — которые используют в своей работе графические элементы управления X-Windows.

Команды системного администрирования

Примеры использования большинства этих команд вы найдёте в сценариях начальной загрузки и остановки системы, в каталогах /etc/rc.d. Они, обычно, вызываются пользователем root и используются для администрирования системы или восстановления файловой системы. Эти команды должны использоваться с большой осторожностью, так как некоторые из них могут разрушить систему, при неправильном использовании.
Пользователи и группы
users
Выведет список всех зарегистрировавшихся пользователей. Она, до некоторой степени,является эквивалентом команды who -q.
groups
Выводит список групп, в состав которых входит текущий пользователь. Эта команда соответствует внутренней переменной $GROUPS, но выводит названия групп, а не их числовые идентификаторы.

bash$ groups
bozita cdrom cdwriter audio xgrpbash
$ 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 удаляет учетную запись пользователя из системы, и, соответствующие файлы. Команда adduser является синонимом для useradd и, как правило, является обычной символической ссылкой на useradd.
usermod
Модификация (изменение) характеристик пользовательского аккаунта. Может изменить пароль пользователя, членство в группах срок действия аккаунта и другие характеристики. С помощью этой команды можно заблокировать пользовательский пароль, что равносильно блокировке аккаунта.
groupmod
Модификация (изменение) характеристик группы. С помощью этой команды можно изменить имя группы и/или ID группы. 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

Команда id выводит эффективный идентификатор только тогда, когда он отличается от реального.
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

Команда logname выводит имя пользователя, зарегистрировавшегося в системе(залогировавшегося), в то время как whoami — даёт имя пользователя, под которым исполняется текущий процесс. Как я уже упоминал — это не всегда одно и то же.

bash$ whoami
bozo

Однако...

bash$ su
Password: ......
bash# whoami
root
bash# logname
bozo

su
Команда предназначена для запуска программы или сценария от имени другого пользователя. su rjones — запускает командную оболочку от имени пользователя rjones. Запуск команды su без параметров означает запуск командной оболочки от имени привилегированного пользователя root.
sudo
Исполняет заданную команду от имени пользователя root (или другого пользователя).

#!/bin/bash
# Доступ к "секретным" файлам.
sudo cp /root/secretfile /home/bozo/secret

Имена пользователей, которым разрешено использовать команду sudo, хранятся в файле /etc/sudoers.
passwd
Устанавливает или изменяет пароль пользователя. Команда passwd может использоваться в сценариях, но это плохая практика.

Пример: Установка нового пароля.

#!/bin/bash
# setnew-password.sh: Не очень хорошая идея.
# Этот сценарий должен запускаться с правами root, а ещё лучше — вообще не запускать его.
ROOT_UID=0 # Root имеет $UID равный 0.
E_WRONG_USER=65 # Не root?
E_NOSUCHUSER=70
SUCCESS=0
if [ "$UID" -ne "$ROOT_UID" ]
then
 echo; echo "Только root может запускать этот сценарий."; echo
 exit $E_WRONG_USER
else
 echo; echo "Вам не следовало бы запускать этот сценарий."
fi
username=bozo
NEWPASSWORD=security_violation 
# Проверить — есть ли такой пользователь.
cat /etc/passwd | grep -q "$username"
if [ $? -ne $SUCCESS ]
then
 echo "Пользователь $username не найден."
 echo "Пароль не был изменен."
 exit $E_NOSUCHUSER
fi
echo "$NEWPASSWORD" | passwd --stdin "$username"
# Ключ '--stdin' указывает 'passwd'
# получить новый пароль с stdin (или из конвейера).
echo; echo "Пароль пользователя $username изменен!"
# Использование команды 'passwd' в сценариях крайне опасно.
exit 0

Команда passwd с ключами -l, -u и -d выполняет блокировку, разблокировку и удаление пароля пользователя. Только root может вызывать команду passwd с этими ключами.
ac
Выводит время работы пользователей, основываясь на записях в файле /var/log/wtmp. Это одна из утилит пакета GNU acct.

bash$ ac
total 68.08

last
Выводит информацию о последних входах/выходах пользователей в сиcтему, основываясь на записях в файле /var/log/wtmp. Эта команда может отображать информацию об удаленных (в смысле — с удалённого терминала) соединениях.
newgrp
Позволяет сменить активную группу пользователя. Пользователь остается в системе и текущий каталог не изменяется, но права доступа к файлам вычисляются в соответствии с новыми реальным и эффективным идентификаторами группы. Эта команда используется довольно редко, так как пользователь, обычно, является членом нескольких групп.
Терминалы
tty
Выводит имя терминала текущего пользователя. Обратите внимание: каждое отдельное окно xterm считается отдельным терминалом.

bash$ tty
/dev/pts/1

stty
Выводит и/или изменяет настройки терминала. Эта сложная команда используется в сценариях для управления поведением терминала.

Пример: Установка символа "забоя".

#!/bin/bash
# erase.sh: Использование команды "stty" для смены клавиши "забоя" при чтении ввода.
echo -n "Как Вас зовут? "
read name                    # Попробуйте стереть последние символы при вводе.
                             # Всё работает.
echo "Вас зовут $name."
stty erase '#'               # Теперь, чтобы стереть символ нужно использовать клавишу
"#".
echo -n "Как Вас зовут? "
read name                    # Попробуйте стереть последние символы при вводе с помощью
"#".
echo "Вас зовут $name."
exit 0

Пример: Невидимый пароль: Отключение эхо-вывода на терминал.

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

#!/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(S.C.).
exit 0

Терминалы и их режимы работы
Как правило, терминалы работают в каноническом режиме. Когда пользователь нажимает какую-либо клавишу, то соответствующий ей символ не сразу передаётся программе, исполняемой в окне терминала. Этот символ поступает сначала в локальный буфер терминала. Когда пользователь нажимает клавишу ENTER, то тогда всё содержимое буфера передаётся программе.
bash$ stty -a 
speed 9600 baud; rows 36; columns 96; line = 0;
intr = ^C; quit = ^\; erase = ^H; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>;
start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O;
...
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt

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

bash$ cat > filexxx
wha<ctl-W>I<ctl-H>foo bar<ctl-U>hello world<ENTER>
<ctl-D>
bash$ cat filexxx
hello world
bash$ bash$ wc -c < file13

Процесс в терминале получит только 13 символов (12 алфавитных символов и символ перевода строки), хотя пользователь нажал 26 клавиш.
В неканоническом ("сыром") режиме, каждая нажатая клавиша (включая специальные символы редактирования, такие как Ctl-H) сразу же передается исполняющемуся в терминале процессу. Под управлением Bash, базовый терминальный редактор заменяется более сложным терминальным редактором Bash. Например, если вы нажмете комбинацию клавиш Ctl-A в командной строке Bash, то вы не увидите символов ^A, которые выводит терминал, вместо этого Bash получит символ \1, проанализирует его и переместит курсор в начало строки.
©Stephane Chazelas


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. Наверное это очень неприятно, когда, во время работы над текстовым файлом, в окне терминала, прямо среди текста, вдруг появляется предложение заказать пиццу. Поэтому, при работе в многопользовательской системе, вам наверняка захочется отключить доступ к своему терминалу.
wall
Имя этой команды — аббревиатура от "write all", т.е., передать сообщение всем пользователям на все терминалы в сети. Это, в первую очередь, инструмент администратора, который можно использовать, например, для оповещения всех пользователей о предстоящей, в ближайшее время, перезагрузке системы.

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!

Если доступ к терминалу был закрыт командой 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, выводит подробную информацию, содержащую имя системы, имя узла (то есть имя, под которым система известна в сети), версию операционной системы, наименование модификации операционной системы, аппаратную архитектуру.

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.

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

Исполнение этой команды будет завершаться неудачей, если пользователь, вызвавший утилиту, не имеет прав на чтение файла /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 }'
bash$ free -h | grep Mem | awk '{ print $4 }'

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. 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

Эта команда генерирует "уникальный" числовой идентификатор системы. Некоторые программные продукты используют этот идентификатор в процедуре регистрации. К сожалению, при генерации идентификатора, 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 Reporter) выводит очень подробную статистику о функционировании операционной системы. В июне 1999 года, компания Santa Cruz Operation(SCO) опубликовала исходные тексты утилиты. Она, как правило, не входит в базовый комплект пакетов Linux-систем. Но её можно получить в составе пакета sysstat utilities, автор: Sebastien Godard.

bash$ sar
Linux 2.4.9 (brooks.seringas.fr)  09/26/03
10:30:00    CPU      %user    %nice    %system    %iowait   %idle
10:40:00    all      2.21     10.90    65.48      0.00      21.41
10:50:00    all      3.36     0.00     72.36      0.00      24.28
11:00:00    all      1.12     0.00     80.77      0.00      18.11
Average:    all      2.23     3.63     72.87      0.00      21.27
 
14:32:30 LINUX RESTART
 
15:00:00    CPU      %user    %nice    %system    %iowait   %idle
15:10:00    all      8.59     2.40     17.47      0.00      71.54
15:20:00    all      4.07     1.00     11.95      0.00      82.98
15:30:00    all      0.79     2.94     7.56       0.00      88.71
Average:    all      6.33     1.70     14.71      0.00      77.26

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) сообщение от пользователя. Для добавления сообщения пользователь не должен обладать привилегиями суперпользователя.

bash$ logger Experiencing instability in network connection at 23:10, 05/21.
# Теперь попробуйте дать команду 'tail /var/log/messages'.

Встраивая вызов logger в сценарии, вы получаете возможность заносить отладочную информацию в системный журнал /var/log/messages.

bash$ 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 вызывается демоном cron ежедневно. Добавляя соответствующие строки в /etc/logrotate.conf, можно заставить logrotate обрабатывать не только системный журнал, но и ваш личный.
Управление заданиями
ps
Process Statistics: Список исполняющихся в данный момент процессов. Обычно вызывается с ключами ax, вывод команды может быть обработан командами grep или sed, с целью поиска требуемого процесса.

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

Пример: Использование команды pidof при остановке процесса.

#!/bin/bash
# kill-process.sh
NOPROCESS=2
process=xxxyyyzzz # Несуществующий процесс.
# Только в демонстрационных целях...
# ... чтобы не уничтожить этим сценарием какой-нибудь процесс.
#
# Если с помощью этого сценария вы задумаете разрывать связь с Internet, то process=pppd 
t=`pidof $process`    # Поиск pid (process id) процесса $process.
# pid требует команда 'kill' (невозможно остановить процесс, указав его имя).
 
if [ -z "$t" ] # Если процесс с таким именем не найден, то 'pidof' вернет null.
then
 echo "Процесс $process не найден."
 exit $NOPROCESS
fi
kill $t        # В некоторых случаях может потребоваться 'kill -9'.
# Здесь нужно проверить — был ли уничтожен процесс.
# Возможно так: " t=`pidof $process` ".
# Этот сценарий мог бы быть заменён командой
# kill $(pidof -x process_name)
# но это было бы не так поучительно.
exit 0

fuser
Возвращает идентификаторы процессов, использующих указанный файл(ы) или каталог. С ключом -k, завершает найденные процессы. Может с успехом использоваться для защиты системы, особенно в сценариях разграничения доступа к системным службам.
cron
Планировщик заданий. С его помощью выполняются такие задачи, как очистка и удаление устаревших файлов системных журналов, обновление базы данных slocate. Это суперпользовательская версия команды at (хотя любой пользователь может создать собственную таблицу crontab). Эта утилита запускается как фоновый процесс-daemon и выполняет задания, находящиеся в файле /etc/crontab. В некоторых дистрибутивах Linux, в качестве crond используется версия cron от Matthew Dillon.
Управление процессами и загрузкой
init
init — предок (родитель) всех процессов в системе. Вызывается на последнем этапе загрузки системы и определяет уровень загрузки (runlevel) из файла /etc/inittab.
telinit
Символическая ссылка на init — инструмент для смены уровня загрузки (runlevel), как правило используется при обслуживании системы или восстановлении файловой системы. Может быть вызвана только суперпользователем. Эта команда может быть очень опасна, при неумелом обращении — прежде чем использовать её, убедитесь в том, что вы совершенно точно понимаете что делаете!
runlevel
Выводит предыдущий и текущий уровни загрузки (runlevel). Уровень загрузки может иметь одно из 6 значений: 0 — остановка системы, 1 — однопользовательский режим, 2 или 3 — многопользовательский режим, 5 — многопользовательский режим и запуск X Window, 6 — перезагрузка. Уровни загрузки определяются из файла /var/run/utmp.
halt,shutdown,reboot
Набор команд для остановки системы, обычно перед выключением питания.
Команды для работы с сетью
ifconfig
Утилита конфигурирования и запуска сетевых интерфейсов. Чаще всего используется в сценариях начальной загрузки системы, для настройки и запуска сетевых интерфейсов или для их остановки перед остановкой или перезагрузкой.

# Фрагменты кода из /etc/rc.d/init.d/network
# ...
# Проверка сетевой полсистемы.
[ ${NETWORKING} = "no" ] && exit 0
[ -x /sbin/ifconfig ] || exit 0
# ...
for i in $interfaces ; do
if ifconfig $i 2>/dev/null | grep -q "UP" >/dev/null 2>&1 ; then
 action "Останавливается $i: " ./ifdown $i boot
fi 
# Ключ "-q", характерный для GNU-версии "grep", означает "quiet" ("молча"), т.е. подавляет вывод.
# Поэтому нет необходимости переадресовывать вывод на /dev/null.
# ...
echo "В настоящее время активны устройства:"
echo `/sbin/ifconfig | grep ^[a-z] | awk '{print $1}'`
#                            ^^^^^ скобки необходимы для предотвращения подстановки имён файлов (globbing).
# Следующий код делает то же самое.
# echo $(/sbin/ifconfig | awk '/^[a-z]/ { print $1 })'
# echo $(/sbin/ifconfig | sed -e 's/ .*//')
# Спасибо S.C. за комментарии.

iwconfig
Эта команда используется для настройки беспроводной (wireless) сети. Представляет собой эквивалент команды ifconfig, описанной выше.
route
Выводит сведения о таблице маршрутизации ядра или вносит туда изменения.

bash$ route
Destination       Gateway          Genmask           Flags     MSS      Window     irtt    Iface
pm3-67.bozosisp   *                255.255.255.255   UH        40       0          0       ppp0
127.0.0.0         *                255.0.0.0         U         40       0          0       lo
default           pm3-67.bozosisp  0.0.0.0           UG        40       0          0       ppp0

chkconfig
Проверка сетевой конфигурации. Обслуживает список, запускаемых на этапе загрузки, сетевых сервисов, список сервисов хранится в каталогах /etc/rc?.d (строго говоря, chkconfig работает не только с сетевыми сервисами, а с сервисами вообще, не зависимо от того сетевые это службы или нет — прим. перев.). Изначально эта утилита была перенесена в Red Hat Linux из ОС IRIX, chkconfig входит в состав далеко не всех дистрибутивов Linux.

bash$ chkconfig --list
atd    0:off 1:off 2:off 3:on  4:on  5:on  6:off
rwhod  0:off 1:off 2:off 3:off 4:off 5:off 6:off
...

tcpdump
"Сниффер" ("sniffer") сетевых пакетов. Инструмент для перехвата и анализа сетевого трафика по определенным критериям. Дамп трафика ip-пакетов между двумя узлами сети — bozoville и caduceus:

bash$ tcpdump ip host bozoville and caduceus

Конечно же, вывод команды tcpdump может быть проанализирован с помощью команд обработки текста, обсуждавшихся выше. Команды для работы с файловыми системами
mount
Выполняет монтирование файловой системы, обычно на устройстве со сменными носителями, такими как дискеты или CDROM. Файл /etc/fstab содержит перечень доступных для монтирования файловых систем, разделов и устройств, включая опции монтирования, благодаря этому файлу, монтирование может производиться автоматически или вручную. Файл /etc/mtab содержит список смонтированных файловых систем и разделов (включая виртуальные, такие как /proc).
mount -a — монтирует все (all) файловые системы и разделы, перечисленные в /etc/fstab, за исключением тех, которые имеют флаг noauto. Эту команду можно встретить в сценариях начальной загрузки системы из /etc/rc.d (rc.sysinit или нечто похожее).

mount -t iso9660 /dev/cdrom /mnt/cdrom
# Монтирование CDROM
mount /mnt/cdrom
# Более короткий и удобный вариант, если точка монтирования /mnt/cdrom описана в /etc/fstab

Эта команда может даже смонтировать обычный файл как блочное устройство. Достигается это за счёт связывания файла с loopback-устройством. Эту возможность можно использовать для проверки ISO9660 образа компакт-диска перед его записью на болванку.

Пример: Проверка образа CD.

# С правами root...
mkdir /mnt/cdtest                                    # Подготовка точки монтирования.
mount -r -t iso9660 -o loop cd-image.iso /mnt/cdtest # Монтирование образа диска.
# ключ "-o loop" эквивалентен "losetup /dev/loop0"
cd /mnt/cdtest                                       # Теперь проверим образ диска.
ls -alR                                              # Вывод списка файлов

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

umount /mnt/cdrom          # Теперь вы можете извлечь диск из привода.

Утилита automount, если она установлена, может выполнять автоматическое монтирование/размонтирование устройств со сменными носителями, такие как дискеты и компакт-диски. На ноутбуках со сменными устройствами FDD и CD-ROM, такой подход может привести к возникновению определённых проблем.
sync
Принудительный сброс содержимого буферов на жесткий диск (синхронизация содержимого буферов ввода-вывода и устройства-носителя). Несмотря на то, что нет такой уж острой необходимости в этой утилите, тем не менее sync придаёт уверенности системным администраторам или пользователям в том, что их данные будут сохранены на жёстком диске, и не будут потеряны в случае какого-либо сбоя. В былые дни, команда sync; sync (дважды — для абсолютной уверенности) была упреждающей мерой перед перезагрузкой системы. Иногда возникает необходимость принудительной синхронизации буферов ввода-вывода с содержимым на магнитном носителе, как, например, при надежном удалении файла или когда наблюдаются скачки напряжения в сети электроснабжения.
losetup
Устанавливает и конфигурирует loopback-устройства
.

Пример: Создание файловой системы в обычном файле.

SIZE=1048576                        # 1 Мб
head -c $SIZE < /dev/zero > file    # Создаётся файл нужного размера.
losetup /dev/loop0 file             # Файл назначается как loopback-устройство.
mke2fs /dev/loop0                   # Создание файловой системы.
mount -o loop /dev/loop0 /mnt       # Монтирование только что созданной файловой системы.
# Спасибо S.C.

mkswap
Создание swap-раздела или swap-файла. Созданный swap-раздел (файл) нужно затем подключить командой swapon.
swapon,swapoff
Разрешает/запрещает использование swap-раздела (файла). Эта команда обычно используется во время загрузки системы или во время остановки.
mke2fs
Создает файловую систему ext2. Должна вызываться с правами суперпользователя.

Пример: Добавление нового жёсткого диска.

#!/bin/bash         # Добавление в систему второго жёсткого диска.
# Программное конфигурирование. Предполагается, что устройство уже подключено к аппаратуре компьютера.
# Взято из статьи автора документа.
# "Linux Gazette", выпуск #38, http://www.linuxgazette.com.
 
ROOT_UID=0          # Этот сценарий должен запускать только root.
E_NOTROOT=67        # Код ошибки, если сценарий запущен простым пользователем.
 
if [ "$UID" -ne "$ROOT_UID" ]
then
 echo "Для запуска этого сценария вы должны обладать правами root."
 exit $E_NOTROOT
fi                  # Будьте крайне осторожны!
# Если что-то пойдёт не так, то вы можете потерять текущую файловую систему.
 
NEWDISK=/dev/hdb    # Предполагается, что /dev/hdb — это новое устройство. Проверьте!
MOUNTPOINT=/mnt/newdisk # Или выберите иное устройство для монтирования.
fdisk $NEWDISK
mke2fs -cv $NEWDISK1 # Проверка на "плохие" блоки (bad blocks) и подробный вывод.
# Обратите внимание: /dev/hdb1, *не* то же самое, что /dev/hdb!
mkdir $MOUNTPOINT
chmod 777 $MOUNTPOINT # Сделать новое устройство доступным для всех пользователей.
# Теперь проверим...
# mount -t ext2 /dev/hdb1 /mnt/newdisk  
# Попробуйте создать каталог.
# Если получилось — отмонтируйте устройство и продолжим.
# Последний штрих:
# Добавьте следующую строку в /etc/fstab.
# /dev/hdb1 /mnt/newdisk ext2 defaults 1 1
exit 0

tune2fs
Настройка отдельных параметров файловой системы ext2, например счётчик максимального количества монтирований без проверки. Должна вызываться с привилегиями пользователя root. Очень опасная утилита~ Вы можете использовать её только на свой страх и риск,поскольку, по неосторожности, вы запросто можете разрушить файловую систему.
dumpe2fs
Выводит на stdout очень подробную информацию о файловой системе. Должна вызываться с привилегиями пользователя root.

root# dumpe2fs /dev/hda7 | grep 'ount count'
dumpe2fs 1.19, 13-Jul-2000 for EXT2 FS 0.5b, 95/08/09
Mount count: 6 
Maximum mount count: 20

hdparm
Выводит или изменяет параметры настройки жёсткого диска. Должна вызываться с привилегиями пользователя root. Потенциально опасна при неправильном использовании.
fdisk
Создание или изменение таблицы разделов на устройствах хранения информации, обычно — жёстких дисках. Должна вызываться с привилегиями пользователя root. Пользуйтесь этой утилитой с особой осторожностью, т.к. при неправильном использовании можно легко разрушить существующую файловую систему!
fsck,e2fsck,debugfs
Набор команд для проверки, восстановления и отладки файловой системы. fsck: интерфейсная утилита для проверки файловых систем в Unix (может вызывать другие утилиты проверки).
e2fsck: проверка файловой системы ext2.
debugfs: отладчик файловой системы ext2. Одно из применений этой универсальной (и опасной!) утилиты — это восстановление удалённых файлов. Только для опытных пользователей!
Все эти утилиты должны вызываться с привилегиями пользователя root. При неправильном использовании, любая из них может разрушить файловую систему!
badblocks
Выполняет поиск плохих блоков (физические повреждения носителей) на устройствах хранения информации. Эта команда может использоваться для поиска плохих блоков при форматировании вновь устанавливаемых жёстких дисков или для проверки устройств резервного копирования.Например, badblocks /dev/fd0, проверит дискету на наличие повреждённых блоков. Утилита badblocks может быть вызвана в деструктивном (проверка осуществляется путём записи некоторого шаблона в каждый блок, а затем производится попытка чтения этого блока) или в недеструктивном (неразрушающем — только чтение) режиме.
lsusb,usbmodules
Команда lsusb выводит сведения о имеющихся в системе шинах USB (Universal Serial Bus) и подключенных к ним устройствах.
Команда usbmodules выводит информацию о модулях драйверов присоединенных USB-устройств.

root# lsusb
Bus 001 Device 001: ID 0000:0000
Device Descriptor:
bLength            18
bDescriptorType    1
bcdUSB             1.00
bDeviceClass       9 Hub
bDeviceSubClass    0
bDeviceProtocol    0
bMaxPacketSize0    8
idVendor           0x0000
idProduct          0x0000
. . .

mkbootdisk
Создание загрузочной дискеты, которая может быть использована для загрузки системы, если, например, была повреждена MBR (master boot record — главная загрузочная запись). Команда mkbootdisk — это сценарий на языке командной оболочки Bash, автор: Erik Troan, располагается в каталоге /sbin.
chroot
CHange ROOT — смена корневого каталога. Обычно, команды и утилиты ориентируются в файловой системе посредством переменной $PATH
, относительно корневого каталога / . Команда chroot изменяет корневой каталог по-умолчанию на другой (рабочий каталог также изменяется). Эта утилита, как правило, используется с целью защиты системы, например, с её помощью можно ограничить доступ к разделам файловой системы для пользователей, подключающихся к системе с помощью telnet (это называется — "поместить пользователя в chroot окружение"). Обратите внимание: после выполнения команды chroot изменяется путь к исполняемым файлам системы.
Команда chroot /opt приведёт к тому, что все обращения к каталогу /usr/bin будут переводиться на каталог /opt/usr/bin. Аналогично, chroot /aaa/bbb /bin/ls будет пытаться вызвать команду ls из каталога /aaa/bbb/bin, при этом, корневым каталогом для ls станет каталог /aaa/bbb. Поместив строчку alias XX 'chroot /aaa/bbb ls' в пользовательский ~/.bashrc, можно эффективно ограничить доступ команде "XX", запускаемой пользователем, к разделам файловой системы. При изменении корневого каталога, вам наверняка потребуется скопировать системные утилиты и разделяемые библиотеки в новый корневой каталог, поскольку после смены корневого каталога, директории с системными утилитами могут оказаться за пределами нового корневого каталога.
lockfile
Эта утилита входит в состав пакета procmail (www.procmail.org). Она создаёт lock file, файл-семафор (или, если угодно, файл блокировки), который управляет доступом к заданному файлу, устройству или ресурсу. Lock file служит признаком того, что данный файл, устройство или ресурс "занят" некоторым процессом, и ограничивает (или вообще запрещает) доступ к ресурсу другим процессам.
Файлы блокировок широко применяются для защиты системных почтовых каталогов от одновременной записи несколькими пользователями, для индикации занятости порта модема, и т.п. Сценарии могут использовать файлы блокировок для того, чтобы выяснить — запущен ли тот или иной процесс. Обратите внимание: если в сценарии будет предпринята попытка создать файл блокировки, когда он уже существует, то такой сценарий скорее всего зависнет. Как правило, файлы блокировки создаются в каталоге /var/lock. Проверка наличия файла блокировки может быть проверена примерно таким образом:

appname=xyzip
# Приложение "xyzip" создает файл блокировки "/var/lock/xyzip.lock".
if [ -e "/var/lock/$appname.lock ]
then
 ...

mknod
Создаёт специальный файл для блочного или символьного устройства (может потребоваться при установке новых устройств в компьютер). В системе имеется более удобная в обращении утилита MAKEDEV, которая обладает всей функциональностью команды mknod.
tmpwatch
Автоматически удаляет файлы, к которым не было обращений в течение заданного периода времени. Обычно вызывается демоном cron для удаления устаревших файлов системного журнала.
MAKEDEV
Утилита предназначена для создания файлов-устройств. Должна запускаться с привилегиями пользователя root, в каталоге /dev.

root# ./MAKEDEV

Это своего рода расширенная версия утилиты mknod.
Команды резервного копирования
dump,restore
Команда dump создаёт резервные копии целых файловых систем, обычно используется в крупных системах и сетях. Она считывает дисковые разделы и сохраняет их в файле, в двоичном формате. Созданные таким образом файлы, могут быть сохранены на каком-либо носителе — на жёстком диске или магнитной ленте. Команда restore — "разворачивает" файлы, созданные утилитой dump.
fdformat
Выполняет низкоуровневое форматирование дискет.
Команды управления системными ресурсами
ulimit
Устанавливает верхний предел для системных ресурсов. Как правило вызывается с ключом -f, что означает наложение ограничений на размер файлов (ulimit -f 1000 ограничит размер вновь создаваемых файлов одним мегабайтом). Ключ -c ограничивает размер файлов coredump (ulimit -c 0 запретит создание coredump-файлов). Обычно, все ограничения прописываются в файле /etc/profile и/или ~/.bash_profile. Грамотное использование ulimit поможет избежать нападений, целью которых является исчерпание системных ресурсов, известных под названием fork bomb.

#!/bin/bash
# Этот сценарий служит исключительно демонстрационным целям!
# Запускайте его на свой страх и риск — он ПОДВЕСИТ вашу систему!
while true # Бесконечный цикл.
do
 $0 & # Этот сценарий вызывает сам себя . . .
      # порождая дочерние процессы бесконечное число раз . . .
      # точнее — до тех пор, пока не иссякнут системные ресурсы.
done # Это печально известный сценарий "sorcerer's appentice".
exit 0 # Сценарий никогда не завершит свою работу.

Команда ulimit -Hu XX (где XX — это верхний предел количества процессов, которые может запустить пользователь одновременно) в /etc/profile вызовет аварийное завершение этого сценария, когда количество процессов превысит установленный предел.
setquota
Команда управления дисковыми квотами для пользователей и групп.
umask
Установка маски режима создания файлов. Накладывает ограничения на атрибуты по-умолчанию для создаваемых файлов. Маска представляет собой восьмеричное значение и определяет запрещённые атрибуты файла. Например, umask 022 удаляет права на запись для группы и прочих пользователей (у файлов, создававшихся с режимом 777, он оказывается равным 755, а режим 666 преобразуется в 644, т.е. 777 NAND 022 = 755, 666 NAND 022 = 644). Конечно же, в последствие, пользователь может откорректировать права доступа к своему файлу с помощью команды chmod. Как правило, значение umask устанавливается в файле /etc/profile и/или ~/.bash_profile.
rdev
Выводит или изменяет корневое устройство, размер RAM-диска или видео режим.Функциональные возможности утилиты rdev вообще повторяются загрузчиком lilo, но rdev по прежнему остаётся востребованной, например, при установке электронного диска (RAM-диск).Это потенциально опасная, при неумелом использовании, утилита.
Команды для работы с модулями ядра
lsmod
Выводит список загруженных модулей.

bash$ lsmod
Module    Size   Used by
autofs    9456   2 (autoclean)
opl3      11376  0
serial_cs 5456   0 (unused)
sb        34752  0
uart401   6384   0 [sb]
sound     58368  0 [opl3 sb uart401]
soundlow  464    0 [sound]
soundcore 2800   6 [sb sound]
ds        6448   2 [serial_cs]
i82365    22928  2

Команда cat /proc/modules выведет на экран эту же информацию.
insmod
Принудительная загрузка модуля ядра (старайтесь вместо insmod использовать команду modprobe). Должна вызываться с привилегиями пользователя root.
rmmod
Выгружает модуль ядра. Должна вызываться с привилегиями пользователя root.
modprobe
Загрузчик модулей, который обычно вызывается из сценариев начальной загрузки системы. Должна вызываться с привилегиями пользователя root.
depmod
Создаёт файл зависимостей между модулями, обычно вызывается из сценариев начальной загрузки системы.
modinfo
Выводит сведения о загруженных модулях ядра.

bash$ modinfo hid
filename: /lib/modules/2.4.20-6/kernel/drivers/usb/hid.o
description: "USB HID support drivers"
author: "Andreas Gal, Vojtech Pavlik <vojtech@suse.cz>"
license: "GPL"

Прочие команды
env
Запускает указанную программу или сценарий с модифицированными переменными окружения(не изменяя среду системы в целом, изменения касаются только окружения запускаемой программы/сценария). Посредством [varname=xxx], устанавливает значение переменной окружения varname, которая будет доступна из запускаемой программы/сценария. Без параметров — просто выводит список переменных окружения с их значениями. В Bash, и других производных от Bourne shell, имеется возможность установки переменных окружения и запуска программы (или сценария) одной командной строкой.

var1=value1 var2=value2 commandXXX
# $var1 и $var2 — будут определены только в окружении для 'commandXXX'.

В первой строке сценария ("sha-bang") можно указать команду env, если путь к командному интерпретатору не известен.

#! /usr/bin/env perl
print "Этот сценарий, на языке программирования Perl, будет запущен,\n";
print "даже если я не знаю где в системе находится Perl.\n";
# Прекрасно подходит для написания кросс-платформенных сценариев,
# когда Perl может находиться совсем не там, где вы ожидаете.
# Спасибо S.C.

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

bash$ ldd /bin/ls
libc.so.6 => /lib/libc.so.6 (0x4000c000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)

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

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

strip
Удаляет отладочную информацию из исполняемого файла. Это значительно уменьшает размер исполняемого файла, но при этом делает отладку программы невозможной. Эту команду часто можно встретить в Makefile-ах, и редко — в сценариях на языке командной оболочки.
nm
Выводит список символов (используемых в целях отладки), содержащихся в откомпилированном двоичном файле.
rdist
Позволяет на заданных машинах хранить идентичные копии файлов. По умолчанию, rdist просматривает только те файлы, версия которых на удалённых машинах более старая, чем на локальной машине. Это делается сравнением последнего времени модификации и размера файла на локальной машине и на удалённых. А теперь, используя полученные нами знания, попробуем разобраться с одним из системных сценариев. Один из самых коротких и простых — это killall, который вызывается для остановки процессов при перезагрузке или выключении компьютера.

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

#!/bin/sh
# --> Комментарии, начинающиеся с "# -->", добавлены автором документа.
# --> Этот сценарий является частью пакета 'rc'-сценариев
# --> Автор: Miquel van Smoorenburg, <miquels@drinkel.nl.mugnet.org>
# --> Этот сценарий характерен для дистрибутива Red Hat
# --> (в других дистрибутивах может отсутствовать).
# Остановить все ненужные сервисы которые еще работают (собственно, их уже не должно быть, это лишь формальная проверка, на всякий случай)
 
for i in /var/lock/subsys/*; do
 # --> Стандартный заголовок цикла for/in, но, поскольку "do"
 # --> находится в той же самой строке, что и for,
 # --> необходимо разделить их символом ";".
 # Проверяется наличие сценария.
 [ ! -f $i ] && continue
 # --> Очень интересное использование "И-списка", эквивалентно:
 # --> if [ ! -f "$i" ]; then continue
 
 # Получить имя подсистемы.
 subsys=${i#/var/lock/subsys/}
 # --> В данном случае совпадает с именем файла.
 # --> Это точный эквивалент subsys=`basename $i`.
 # --> Таким образом получается имя файла блокировки (если он присутствует,
 # --> то это означает, что процесс запущен).
 # --> См. описание команды "lockfile" выше.
 
 # Остановить службу.
 if [ -f /etc/rc.d/init.d/$subsys.init ]; then
  /etc/rc.d/init.d/$subsys.init stop
 else
  /etc/rc.d/init.d/$subsys stop
 # --> Останавливает задачу или демона
 # --> посредством встроенной команды 'stop'.
 fi
done

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

Подстановка команд

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

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

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

rm `cat filename` # здесь "filename" содержит список удаляемых файлов.
#
# S. C. предупреждает, что в данном случае может возникнуть ошибка "arg list too long".
# Такой вариант будет лучше: xargs rm -- < filename
# ( -- подходит для случая, когда "filename" начинается с символа "-" )
 
textfile_listing=`ls *.txt`
# Переменная содержит имена всех файлов *.txt в текущем каталоге.
echo $textfile_listing
 
textfile_listing2=$(ls *.txt) # Альтернативный вариант.
echo $textfile_listing2
# Результат будет тем же самым.
 
# Проблема записи списка файлов в строковую переменную состоит в том,
# что символы перевода строки заменяются на пробел.
#
# Как вариант решения проблемы -- записывать список файлов в массив.
# shopt -s nullglob # При несоответствии, имя файла игнорируется.
# textfile_listing=( *.txt )
#
# Спасибо S.C.

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

COMMAND `echo a b` # 2 аргумента: a и b
COMMAND "`echo a b`" # 1 аргумент: "a b"
COMMAND `echo` # без аргументов 
COMMAND "`echo`" # один пустой аргумент
# Спасибо S.C.

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

# cd "`pwd`" # Должна выполняться всегда.
# Однако...
mkdir 'dir with trailing newline
'
cd 'dir with trailing newline
'
cd "`pwd`" # Ошибка:
# bash: cd: /tmp/dir with trailing newline: No such file or directory
cd "$PWD" # Выполняется без ошибки.
 
 
old_tty_setting=$(stty -g) # Сохранить настройки терминала.
echo "Нажмите клавишу "
stty -icanon -echo # Запретить "канонический" режим терминала.
                   # Также запрещает эхо-вывод.
key=$(dd bs=1 count=1 2> /dev/null) # Поймать нажатие на клавишу.
stty "$old_tty_setting" # Восстановить настройки терминала.
echo "Количество нажатых клавиш = ${#key}." # ${#variable} = количество символов в переменной $variable
#
# Нажмите любую клавишу, кроме RETURN, на экране появится "Количество нажатых клавиш = 1."
# Нажмите RETURN, и получите: "Количество нажатых клавиш = 0."
# Символ перевода строки будет "съеден" операцией подстановки команды.
Спасибо S.C.

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

dir_listing=`ls -l`
echo $dir_listing # без кавычек
# Вы наверно ожидали увидеть удобочитаемый список каталогов.
 
# Однако, вы получите:
# total 3 -rw-rw-r-- 1 bozo bozo 30 May 13 17:15 1.txt -rw-rw-r-- 1 bozo
# bozo 51 May 15 20:57 t2.sh -rwxr-xr-x 1 bozo bozo 217 Mar 5 21:13 wi.sh
# Символы перевода строки были заменены пробелами.
 
echo "$dir_listing" # в кавычках
# -rw-rw-r-- 1 bozo 30 May 13 17:15 1.txt
# -rw-rw-r-- 1 bozo 51 May 15 20:57 t2.sh
# -rwxr-xr-x 1 bozo 217 Mar 5 21:13 wi.sh

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

variable1=`<file1`    # Записать в переменную "variable1" содержимое файла "file1".
variable2=`cat file2` # Записать в переменную "variable2" содержимое файла "file2".
                      # Однако, эта команда порождает дочерний процесс,
                      # поэтому эта строка выполняется медленнее, чем предыдущая.
# Замечание:
# В переменные можно записать даже управляющие символы.
 
# Выдержки из системного файла /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

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

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

#!/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, в теле цикла.

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

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

Подстановка команд позволяет существенно расширить набор инструментальных средств, которыми располагает Bash. Суть состоит в том, чтобы написать программу или сценарий, которая выводит результаты своей работы на stdout (как это делает подавляющее большинство утилит в Unix) и записать вывод от программы в переменную.
#include <stdio.h>
/* Программа на C "Hello, world." */
int main()
{
 printf( "Hello, world." );
 return (0);
}
bash$ gcc -o hello hello.c
#!/bin/bash
# hello.sh
greeting=`./hello`
echo $greeting
bash$ sh hello.sh
Hello, world.


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

output=$(sed -n /"$1"/p $file) # К примеру из "grp.sh".
 
# Запись в переменную содержимого текстового файла.
File_contents1=$(cat $file1)
File_contents2=$(<$file2) # Bash допускает и такую запись.

Следует упомянуть, что в конструкции $(...) два идущих подряд обратных слэша интерпретируются несколько иначе, чем в конструкции `...`.

bash$ echo `echo \\`
 
bash$ echo $(echo \\)
\

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

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

z=`expr $z + 3` # Команда 'expr' вычисляет значение выражения.

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

z=$(($z+3))
z=$((z+3) # Тоже верно.
# Внутри двойных круглых скобок, переменные разыменовываются автоматически.
# $((EXPRESSION)) — это подстановка арифметического выражения!
# Не путайте с подстановкой команд!
# Внутри двойных круглых скобок допускается выполнение арифметических
# действий без использования оператора присваивания
n=0
echo "n = $n" # n = 0
(( n += 1 )) # Инкремент.
# (( $n += 1 )) это неверно!
echo "n = $n" # n = 1
let z=z+3
let "z += 3" # Кавычки позволяют вставлять пробелы
             # Оператор 'let' вычисляет арифметическое выражение,
             # это не подстановка арифметического выражения.

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

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

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". 249
  # Если "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

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

ls -yz >> command.log 2>&1
# Сообщение о неверной опции "yz" в команде "ls" будет записано в файл "command.log".
# Поскольку stderr перенаправлен в файл.
 
# Обратите внимание: следующая строка даст иной результат.
ls -yz 2>&1 >> command.log
# Сообщение об ошибке не попадет в файл.
# Если производится перенаправление обоих устройств, stdout и 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.

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

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

Пример: Перенаправление 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, теперь выводится в этот файл.

Пример: Перенаправление 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

Пример: Одновременное перенаправление устройств, 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

Перенаправление для блоков кода

Блоки кода, такие как циклы while, until и for, условный оператор if/then, так же могут смешиваться с перенаправлением stdin. Даже функции могут использовать эту форму перенаправления. Оператор перенаправления <, в таких случаях, ставится в конце блока.

Пример: Перенаправление в цикл while.

#!/bin/bash
if [ -z "$1" ]
then
 Filename=names.data # По-умолчанию, если имя файла не задано.
else
 Filename=$1
fi
# Конструкцию проверки выше, можно заменить следующей строкой (подстановка параметров):
# Filename=${1:-names.data}
 
count=0
echo
while [ "$name" != Smith ] # Почему переменная $name взята в кавычки?
do
read name # Чтение из $Filename, не со stdin.
echo $name
let "count += 1"
done <"$Filename" # Перенаправление на ввод из файла $Filename.
#    ^^^^^^^^^^^^
 
echo; echo "Имён прочитано: $count"; echo
# Обратите внимание: в некоторых старых командных интерпретаторах,
# перенаправление в циклы приводит к запуску цикла в субоболочке (subshell).
# Таким образом, переменная $count, по окончании цикла, будет содержать 0,
# значение, записанное в неё до входа в цикл.
# Bash и ksh стремятся избежать запуска субоболочки (subshell), если это возможно,
# так что этот сценарий, в этих оболочках, работает корректно.
#
# Спасибо Heiner Steven за это примечание.
exit 0

Пример: Альтернативная форма перенаправления в цикле while.

#!/bin/bash
# Это альтернативный вариант предыдущего сценария.
# Предложил: by Heiner Steven
# для случаев, когда циклы с перенаправлением
# запускаются в субоболочке, из-за чего переменные, устанавливаемые в цикле,
# не сохраняют свои значения по завершении цикла.
 
if [ -z "$1" ]
then
 Filename=names.data # По-умолчанию, если имя файла не задано.
else
 Filename=$1
fi
exec 3<&0 # Сохранить stdin в дескр. 3.
exec 0<"$Filename" # Перенаправить stdin.
 
count=0
echo
while [ "$name" != Smith ]
do
 read name # Прочитать с перенаправленного stdin ($Filename).
 echo $name
 let "count += 1"
done <"$Filename" # Цикл читает из файла $Filename.
#    ^^^^^^^^^^^^
 
exec 0<&3 # Восстановить stdin.
exec 3<&- # Закрыть временный дескриптор 3.
echo; echo "Имен прочитано: $count"; echo
 
exit 0

Пример: Перенаправление в цикл until.

#!/bin/bash
# То же самое, что и в предыдущем примере, только для цикла "until".
 
if [ -z "$1" ]
then
 Filename=names.data # По-умолчанию, если файл не задан.
else
 Filename=$1
fi
# while [ "$name" != Smith ]
until [ "$name" = Smith ] # Проверка != изменена на =.
do
 read name # Чтение из $Filename, не со stdin.
 echo $name
done <"$Filename" # Перенаправление на ввод из файла $Filename.
#    ^^^^^^^^^^^^
 
# Результаты получаются теми же, что и в случае с циклом "while", в предыдущем примере.
exit 0

Пример: Перенаправление в цикл for.

#!/bin/bash
if [ -z "$1" ]
then
 Filename=names.data # По-умолчанию, если файл не задан.
else
 Filename=$1
fi
line_count=`wc $Filename | awk '{ print $1 }'`
#           Число строк в файле.
# Слишком запутано, тем не менее показывает
# возможность перенаправления stdin внутри цикла "for"...
# если вы достаточно умны.
# Более короткий вариант line_count=$(wc < "$Filename")
 
for name in `seq $line_count` # "seq" выводит последовательность чисел.
# while [ "$name" != Smith ] — более запутанно, чем в случае с циклом "while" 
do
 read name # Чтение из файла $Filename, не со stdin.
 echo $name
 if [ "$name" = Smith ]
 then
  break
 fi
done <"$Filename" # Перенаправление на ввод из файла $Filename.
#    ^^^^^^^^^^^^
exit 0

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

Пример: Перенаправление устройств (stdin и stdout) в цикле for.

#!/bin/bash
if [ -z "$1" ]
then
 Filename=names.data # По-умолчанию, если файл не задан.
else
 Filename=$1
fi
Savefile=$Filename.new # Имя файла, в котором сохраняются результаты.
FinalName=Jonah # Имя, на котором завершается чтение.
 
line_count=`wc $Filename | awk '{ print $1 }'` # Число строк в заданном файле.
 
for name in `seq $line_count`
do
 read name
 echo "$name"
 if [ "$name" = "$FinalName" ]
 then
  break
 fi
done < "$Filename" > "$Savefile" # Перенаправление на ввод из файла $Filename,
#    ^^^^^^^^^^^^^^^^^^^^^^^^^^^ и сохранение результатов в файле.
exit 0

Пример: Перенаправление в конструкции if/then.

#!/bin/bash
if [ -z "$1" ]
then
 Filename=names.data # По-умолчанию, если файл не задан.
else
 Filename=$1
fi
TRUE=1
if [ "$TRUE" ] # конструкции "if true" и "if :" тоже вполне допустимы.
then
 read name
 echo $name
fi <"$Filename"
#  ^^^^^^^^^^^^
# Читает только первую строку из файла.
exit 0

Пример: Файл с именами "names.data", для примеров выше.

Aristotle
Belisarius
Capablanca
Euler
Goethe
Hamurabi
Jonah
Laplace
Maroczy
Purcell
Schmidt
Semmelweiss
Smith
Turing
Venn
Wilson
Znosko-Borowski
# Это файл с именами для примеров
# "redir2.sh", "redir3.sh", "redir4.sh", "redir4a.sh", "redir5.sh".

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

Область применения

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

Пример: Регистрация событий.

#!/bin/bash
# logevents.sh, автор: Stephane Chazelas.
# Регистрация событий в файле.
# Сценарий должен запускаться с привилегиями root (что бы иметь право на запись в /var/log).
 
ROOT_UID=0 # Привилегии root имеет только пользователь с $UID = 0.
E_NOTROOT=67 # Код завершения, если не root.
 
if [ "$UID" -ne "$ROOT_UID" ]
then
 echo "Сценарий должен запускаться с привилегиями root."
 exit $E_NOTROOT
fi
FD_DEBUG1=3
FD_DEBUG2=4
FD_DEBUG3=5
 
# Раскомментируйте одну из двух строк, ниже, для активизации сценария.
# LOG_EVENTS=1
# LOG_VARS=1log() # Запись даты и времени в файл.
 
log() # Запись даты и времени в файл.
{
 echo "$(date) $*" >&7 # Добавляет в конец файла.
}
 
case $LOG_LEVEL in
 1) exec 3>&2 4> /dev/null 5> /dev/null;;
 2) exec 3>&2 4>&2 5> /dev/null;;
 3) exec 3>&2 4>&2 5>&2;;
 *) exec 3> /dev/null 4> /dev/null 5> /dev/null;;
esac
FD_LOGVARS=6
if [[ $LOG_VARS ]]
then exec 6>> /var/log/vars.log
else exec 6> /dev/null # Подавить вывод.
fi
 
FD_LOGEVENTS=7
if [[ $LOG_EVENTS ]]
then
 # then exec 7 >(exec gawk '{print strftime(), $0}' >> /var/log/event.log)
 # Строка, выше, не работает в Bash, версии 2.04.
 exec 7>> /var/log/event.log # Добавление в конец "event.log".
 log # Записать дату и время.
 else exec 7> /dev/null # Подавить вывод.
fi
echo "DEBUG3: beginning" >&${FD_DEBUG3}
ls -l >&5 2>&4 # command1 >&5 2>&4
echo "Done" # command2
echo "sending mail" >&${FD_LOGEVENTS} # Написать "sending mail" в дескр. #7.
 
exit 0

Встроенные документы

Встроенный документ (here document) является специальной формой перенаправления ввода/вывода, которая позволяет передать список команд интерактивной программе или команде, например ftp, telnet или ex.

COMMAND <<InputComesFromHERE
...
InputComesFromHERE

Конец встроенного документа выделяется "строкой-ограничителем", которая задается с помощью специальной последовательности символов <<. Эта последовательность — есть перенаправление вывода из файла на stdin программы или команды, что напоминает конструкцию interactive-program< command-file, где command-file содержит строки:

command #1
command #2
...

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

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

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

Пример: 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-сценарии.

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

#!/bin/bash
wall <<zzz23EndOfMessagezzz23
Пошлите, по электронной почте, ваш заказ на пиццу, системному администратору.
(Добавьте дополнительный доллар, если вы желаете положить на пиццу анчоусы или грибы.)
# Внимание: строки комментария тоже будут переданы команде 'wall' как часть текста.
zzz23EndOfMessagezzz23
 
# Возможно, более эффективно это может быть сделано так:
# wall <message-file
# Однако, встроенный документ помогает сэкономить ваши силы и время.
exit 0

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

#!/bin/bash
# Команда 'echo' прекрасно справляется с выводом однострочных сообщений,
# но иногда необходимо вывести несколько строк.
# Команда 'cat' и встроенный документ помогут вам в этом.
cat <<End-of-message
-------------------------------------
Это первая строка сообщения.
Это вторая строка сообщения.
Это третья строка сообщения.
Это четвертая строка сообщения.
Это последняя строка сообщения.
-------------------------------------
End-of-message
exit 0
#--------------------------------------------
# Команда "exit 0", выше, не позволить исполнить нижележащие строки.
# S.C. отмечает, что следующий код работает точно так же.
echo "-------------------------------------
Это первая строка сообщения.
Это вторая строка сообщения.
Это третья строка сообщения.
Это четвертая строка сообщения.
Это последняя строка сообщения.
-------------------------------------"
# Однако, в этом случае, двойные кавычки в теле сообщения, должны экранироваться.

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

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

#!/bin/bash
# То же, что и предыдущий сценарий, но...
# Символ "-", начинающий строку-ограничитель встроенного документа: <<-
# подавляет вывод символов табуляции, которые могут встречаться в теле документа,
# но не пробелов.
 
cat <<-ENDOFMESSAGE
        Это первая строка сообщения.
        Это вторая строка сообщения.
        Это третья строка сообщения.
        Это четвертая строка сообщения.
        Это последняя строка сообщения.
ENDOFMESSAGE
# Текст, выводимый сценарием, будет смещен влево.
# Ведущие символы табуляции не будут выводиться.
 
# Вышеприведённые 5 строк текста "сообщения" начинаются с табуляции, а не с пробелов.
exit 0

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

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

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

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

Пример: Передача пары файлов во входящий каталог на "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

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

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

#!/bin/bash
# Вывод встроенного документа командой 'cat', с запретом подстановки параметров.
 
NAME="John Doe"
RESPONDENT="автора этого сценария"
 
cat <<'Endofmessage'
 
Привет, $NAME.
Примите поздравления от $RESPONDENT.
Endofmessage
# Подстановка параметров не производится, если строка ограничитель заключена в кавычки или экранирована.
# Тот же эффект дают:
# cat <<"Endofmessage"
# cat <<\Endofmessage
exit 0

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

Пример: Сценарий, который создаёт другой сценарий.

#!/bin/bash
# generate-script.sh
# Автор идеи: Albert Reiner.
OUTFILE=generated.sh # Имя нового сценария.
 
# -----------------------------------------------------------
# 'Встроенный документ' содержит тело создаваемого сценария.
(
cat <<'EOF'
#!/bin/bash
 
echo "Этот сценарий сгенерирован автоматически."
# Обратите внимание: поскольку действия происходят в подоболочке,
# мы не можем получить доступ к переменным родительской оболочки.
# Удостоверимся в этом...
echo "Файл сценария был назван: $OUTFILE" # Не работает.
a=7
b=3
let "c = $a * $b"
echo "c = $c"
exit 0
EOF
) > $OUTFILE
# -----------------------------------------------------------
 
# Заключение 'строки-ограничителя' предотвращает подстановку значений переменных в теле 'встроенного документа.'
# Что позволяет записать все строки в выходной файл "один к одному".
 
if [ -f "$OUTFILE" ]
then
 chmod 755 $OUTFILE
 # Дать право на исполнение.
else
 echo "Не могу создать файл: \"$OUTFILE\""
fi
# Этот метод можно использовать для создания Makefile-ов, программ на языках C, Perl, Python и т.п.
 
exit 0

Допускается запись тела встроенного документа в переменную.

variable=$(cat <<SETVAR
Это многострочная переменная.
SETVAR)
echo "$variable"

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

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

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

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

Пример: "Анонимный" встроенный документ.

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

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

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

#!/bin/bash
# commentblock.sh
: << COMMENTBLOCK
echo "Эта строка не будет выведена."
Эта строка комментария не начинается с символа "#".
Это ещё одна строка комментария, которая начинается не с символа "#".
 
&*@!!++=
Эта строка не вызовет ошибки,
поскольку Bash проигнорирует её.
COMMENTBLOCK
echo "Код завершения \"COMMENTBLOCK\" = $?." # 0
# Показывает, что ошибок не возникало.
 
# Такая методика создания блочных комментариев может использоваться для комментирования блоков кода во время отладки.
# Это экономит силы и время, т.к. не нужно вставлять символ "#" в начале каждой строки, а затем удалять их.
 
: << DEBUGXXX
for file in *
do
 cat "$file"
done
DEBUGXXX
 
exit 0

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

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

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

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

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

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

#!/bin/bash
echo "----------------------------------------------------------------------"
cat <<LimitString
echo "Это первая строка сообщения во встроенном документе."
echo "Это вторая строка сообщения во встроенном документе."
echo "Это последняя строка сообщения во встроенном документе."
     LimitString
#^^^^Отступ перед строкой-ограничителем. Ошибка!
#    Этот сценарий будет вести себя не так как вы ожидаете.
 
echo "----------------------------------------------------------------------"
# "Этот комментарий находится за пределами 'встроенного документа', и не должен выводиться.
echo "За пределами встроенного документа."
 
exit 0
 
echo "Держу пари, что эта строка не будет выведена." # Стоит после команды 'exit'.

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

Часть четвёртая. Материал повышенной сложности

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

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

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

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

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

  • Звёздочка — * — означает любое количество символов в строке, предшествующих "звёздочке", в том числе и нулевое число символов.
Выражение "113*" — означает 11 + один или более символов "3" + любые другие символы: 113,1133, 113312, и так далее.
  • Точка — . — означает не менее одного любого символа, за исключением символа перевода строки — \n.
Выражение "13." будет означать 13 + по меньшей мере один любой символ (включая пробел): 1133,11333 и т.д., но не 13 (отсутствуют дополнительные символы).
  • Символ — ^ — означает начало строки, но иногда, в зависимости от контекста, означает отрицание в регулярных выражениях.
  • Знак доллара — $ — в конце регулярного выражения соответствует концу строки.
Выражение "^$" соответствует пустой строке.
  • Квадратные скобки — [...] — предназначены для задания подмножества символов. Квадратные скобки, внутри регулярного выражения, считаются одним символом, который может принимать значения, перечисленные внутри этих скобок.
Выражение "[xyz]" — соответствует одному из символов x, y или z.
Выражение "[c-n]" соответствует одному из символов в диапазоне от c до n, включительно.
Выражение "[B-Pk-y]" соответствует одному из символов в диапазоне от B до P или в диапазоне от k до y, включительно.
Выражение "[a-z0-9]" соответствует одному из символов латиницы в нижнем регистре или цифре.
Выражение "[^b-d]" соответствует любому символу, кроме символов из диапазона от b до d, включительно. В данном случае, метасимвол ^ означает отрицание.

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

Так, выражение "[Yy][Ee][Ss]" соответствует словам yes, Yes, YES, yEs и так далее.
Выражение "[0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]" определяет шаблон для поиска любого номера карточки социального страхования (для США).
  • Обратный слэш — \ — служит для экранирования специальных символов, это означает, что экранированные символы должны интерпретироваться буквально, т.е. как простые символы.
Комбинация "\$" указывает на то, что символ "$" трактуется как обычный символ, а не как признак конца строки в регулярных выражениях. Аналогично, комбинация "\\" соответствует простому символу "\".
  • Экранированные "угловые скобки" — \<...\> — отмечают границы слова.
Угловые скобки должны экранироваться, иначе они будут интерпретироваться как простые символы.
Выражение "\<the\>" соответствует слову "the", и не соответствует словам "them", "there", "other" и т.п.
bash$ cat textfile
This is line 1, of which there is only one instance.
This is the only instance of line 2.
This is line 3, another line.
This is line 4.
 
bash$ grep 'the' textfile
This is line 1, of which there is only one instance.
This is the only instance of line 2.
This is line 3, another line.
 
bash$ grep '\<the\>' textfile
This is the only instance of line 2.

Дополнительные метасимволы. Использующиеся при работе с egrep, awk и Perl

  • Знак вопроса — ? — означает, что предыдущий символ или регулярное выражение встречается 0 или 1 раз. В основном используется для поиска одиночных символов.
  • Знак "плюс" — + — указывает на то, что предыдущий символ или выражение встречается 1 или более раз. Играет ту же роль, что и символ "звёздочка" (*), за исключением случая нулевого количества вхождений.
# GNU версии sed и awk допускают использование "+", но его необходимо экранировать.
 
echo a111b | sed -ne '/a1\+b/p'
echo a111b | grep 'a1\+b'
echo a111b | gawk '/a1+b/'
# Все три варианта эквивалентны.
# Спасибо S.C.

  • Экранированные "фигурные скобки" — \{ \} — задают число вхождений предыдущего выражения.
Экранирование фигурных скобок — обязательное условие, иначе они будут интерпретироваться как простые символы. Такой порядок использования, технически, не является частью основного набора правил построения регулярных выражений.
Выражение "[0-9]\{5\}" — в точности соответствует подстроке из пяти десятичных цифр(символов из диапазона от 0 до 9, включительно).

В "классической" (не совместимой с POSIX) версии awk, фигурные скобки не могут быть использованы. Однако, в gawk предусмотрен ключ --re-interval, который позволяет использовать (неэкранированные) фигурные скобки.

bash$ echo 2222 | gawk --re-interval '/2{3}/'
2222

Язык программирования Perl и некоторые версии egrep не требуют экранирования фигурных скобок.

  • Круглые скобки — ( ) — предназначены для выделения групп регулярных выражений. Они полезны при использовании с оператором "|" и при извлечении подстроки с помощью команды expr.
  • Вертикальная черта — | — выполняет роль логического оператора "ИЛИ" в регулярных выражениях и служит для задания набора альтернатив.
bash$ egrep 're(a|e)d' misc.txt
People who read seem to be better informed than those who do not.
The clarinet produces sound by the vibration of its reed.

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

Классы символов POSIX. [:class:]
Это альтернативный способ указания диапазона символов.

  • Класс [:alnum:] — соответствует алфавитным символам и цифрам. Эквивалентно выражению A-Za-z0-9.
  • Класс [:alpha:] — соответствует символам алфавита. Эквивалентно выражению A-Za-z.
  • Класс [:blank:] — соответствует символу пробела или символу табуляции.
  • Класс [:cntrl:] — соответствует управляющим символам (control characters).
  • Класс [:digit:] — соответствует набору десятичных цифр. Эквивалентно выражению 0-9.
  • Класс [:graph:] — (печатаемые и псевдографические символы) — соответствует набору символов из диапазона ASCII 33 - 126. Это то же самое, что и класс [:print:], за исключением символа пробела.
  • Класс [:lower:] — соответствует набору алфавитных символов в нижнем регистре.Эквивалентно выражению a-z.
  • Класс [:print:] — (печатаемые символы) — соответствует набору символов из диапазона ASCII 32 - 126. По своему составу этот класс идентичен классу [:graph:], описанному выше, за исключением того, что в этом классе дополнительно присутствует символ пробела.
  • Класс [:space:] — соответствует пробельным символам (пробел и горизонтальная табуляция).
  • Класс [:upper:] — соответствует набору символов алфавита в верхнем регистре. Эквивалентно выражению A-Z.
  • Класс [:xdigit:] — соответствует набору шестнадцатиричных цифр. Эквивалентно выражению 0-9A-Fa-f.

Вообще, символьные классы POSIX требуют заключения в кавычки или двойные квадратные скобки ([[ ]]).

bash$ grep [[:digit:]] test.file
abc=723

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

bash$ ls -l ?[[:digit:]][[:digit:]]?
-rw-rw-r-- 1 bozo bozo 0 Aug 21 14:47 a33b

Sed, awk и Perl, используемые в сценариях в качестве фильтров, могут принимать регулярные выражения в качестве входных аргументов.
В качестве стандартного руководства по этой сложной теме можно порекомендовать "MasteringRegular Expressions", автор Friedl. Книга "Sed & Awk" (авторы Dougherty и Robbins) так же даёт полное и ясное описание регулярных выражений (см. раздел Библиография).

Globbing — Подстановка имён файлов

Bash, сам по себе, не распознаёт регулярные выражения. Но в сценариях можно использовать команды и утилиты, такие как sed и awk, которые прекрасно справляются с обработкой регулярных выражений.
Фактически, Bash может выполнять подстановку имён файлов, этот процесс называется "globbing", но при этом не используется стандартный набор регулярных выражений. Вместо этого, при выполнении подстановки имён файлов, производится распознавание и интерпретация шаблонных символов. В число интерпретируемых шаблонов входят символы * и ?, списки символов в квадратных скобках и некоторые специальные символы (например ^, используемый для выполнения операции отрицания). Применение шаблонных символов имеет ряд важных ограничений. Например, если имена файлов начинаются с точки (например так: .bashrc), то они не будут соответствовать шаблону, содержащему символ *. Аналогично, символ ? в операции подстановки имён файлов имеет иной смысл, нежели в регулярных выражениях.

bash$ ls -l
total 2
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 a.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 b.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 c.1
-rw-rw-r-- 1 bozo bozo 466 Aug 6 17:48 t2.sh
-rw-rw-r-- 1 bozo bozo 758 Jul 30 09:02 test1.txt
 
bash$ ls -l t?.sh
-rw-rw-r-- 1 bozo bozo 466 Aug 6 17:48 t2.sh
 
bash$ ls -l [ab]*
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 a.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 b.1
 
bash$ ls -l [a-c]*
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 a.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 b.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 c.1
 
bash$ ls -l [^ab]*
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 c.1
-rw-rw-r-- 1 bozo bozo 466 Aug 6 17:48 t2.sh
-rw-rw-r-- 1 bozo bozo 758 Jul 30 09:02 test1.txt
 
bash$ ls -l {b*,c*,*est*}
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 b.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 c.1
-rw-rw-r-- 1 bozo bozo 758 Jul 30 09:02 test1.txt

Bash выполняет подстановку имён файлов при использовании аргументов командной строки без кавычек. Команда echo наглядно демонстрирует это.

bash$ echo *
a.1 b.1 c.1 t2.sh test1.txt
 
bash$ echo t*
t2.sh test1.txt

Bash предоставляет возможность изменить порядок интерпретации специальных символов вовремя подстановки имён файлов. Так команда set -f запрещает подстановку, а ключи nocaseglob и nullglob в shopt изменяют характер подстановки.

Подоболочки или Subshells

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

Как правило, внешние команды в сценариях порождают дочерние процессы, в то время как встроенные команды Bash — нет. По этой причине, встроенные команды выполняются много быстрее своих "внешних" аналогов.

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

( command1; command2; command3; ... )

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

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

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

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

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

#!/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 # Переменная была инициализирована в данном сценарии
   # или это внутренняя переменная Bash
   # или это переменная окружения (была экспортирована в оболочку).
 
# Можно сделать то же самое по другому: [[ ${variable-x} != x || ${variable-y} != y ]]
# или                                   [[ ${variable-x} != x$variable ]]
# или                                   [[ ${variable+x} = x ]])
# или                                   [[ ${variable-x} != x ]])

Ещё одно применение — проверка файлов блокировки:

if (set -C; : > lock_file) 2> /dev/null
then
 : # lock_file отсутствует: программа (сценарий) не запущена
else
 echo "Этот сценарий уже запущен другим пользователем."
 exit 65
fi
# Автор фрагмента Stephane Chazelas,
# небольшие дополнения сделаны Paulo Marcel Coelho Aragao.

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

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

(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).
Блок команд, заключенный в фигурные скобки не приводит к запуску дочерней подоболочки. { command1; command2; command3; ... }

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

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

В ограниченном режиме запрещена команда cd — смена текущего каталога.
Запрещено изменять переменные окружения $PATH, $SHELL, $BASH_ENV и $ENV.
Запрещён доступ к переменной $SHELLOPTS.
Запрещено перенаправление вывода.
Запрещён вызов утилит, в названии которых присутствует хотя бы один символ "слэш" (/).
Запрещён вызов команды exec для запуска другого процесса.
Запрещён ряд других команд, которые могут использовать сценарий для выполнения непредусмотренных действий.
Запрещён выход из ограниченного режима.

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

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

Подстановка процессов

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

Шаблон подстановки команды
Внутри круглых скобок:

>(command)
<(command)
Таким образом инициируется подстановка процессов. Здесь, для передачи результата работы процесса в круглых скобках, используются файлы /dev/fd/<n>. Между круглой скобкой и символом "<" или ">", не должно быть пробелов, в противном случае это вызовет сообщение об ошибке.
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>&1tar 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 в подоболочке, этот вариант запускает цикл while в подоболочке,
      # и поэтому переменные не видны за пределами цикла, после закрытия канала.

Функции

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

function function_name {
command...
}

или

function_name () {command...}
Вторая форма записи ближе к сердцу C-программистам (она же более переносимая). Как и в языке C, скобка, открывающая тело функции, может помещаться на следующей строке.
function_name ()
{
command...
}
Вызов функции осуществляется простым указанием её имени в тексте сценария.

Пример: Простые функции.

#!/bin/bash
JUST_A_SECOND=1
funky ()
{ # Это пример простейшей функции, которая выводит сообщение.
 echo "Это функция funky."
} # Объявление функции должно предшествовать её вызову.
 
fun ()
{ # Более сложная функция.
 i=0
 REPEATS=30
 
 echo
 echo "Теперь запускается функция fun."
 echo
 
 sleep $JUST_A_SECOND # Эй! Погодите секундочку!
 while [ $i -lt $REPEATS ]
 do
  echo "------------------РАБОТА------------------->"
  echo "<---------------С ФУНКЦИЯМИ----------------"
  echo "<------------ОЧЕНЬ УВЛЕКАТЕЛЬНА------------>"
  echo
  let "i+=1"
 done
}
# А теперь собственно вызов функций.
funky
fun
 
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.

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

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

function_name $arg1 $arg2

Доступ к входным аргументам, в функциях, производится посредством позиционных параметров, т.е. $1, $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

Команда shift вполне применима и к аргументам функций.
В отличие от других языков программирования, в сценариях на языке командной оболочки, аргументы в функции передаются по значению. Переменные (которые фактически являются указателями) при передаче в функции в виде параметров, интерпретируются как строковые литералы. Функции всегда интерпретируют свои аргументы буквально. Механизм косвенных ссылок на переменные слишком неудобен для передачи аргументов по ссылке.

Пример: Передача косвенных ссылок в функцию.

#!/bin/bash
# ind-func.sh: Передача косвенных ссылок в функцию.
echo_var ()
{
echo "$1"
}
message=Hello
Hello=Goodbye
 
echo_var "$message" # Hello
# А теперь передадим функции косвенную ссылку.
 
echo_var "${!message}" # Goodbyeecho "-------------"
 
echo "-------------"
 
# Что произойдёт, если изменить содержимое переменной "Hello"?
Hello="Hello, again!"
echo_var "$message" # Hello
echo_var "${!message}" # Hello, again!
 
exit 0

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

Пример: Изменение значения переменной, переданной в функцию по ссылке.

#!/bin/bash
# dereference.sh
# Изменение значения переменной, переданной в функцию по ссылке.
# Автор: Bruce W. Clare.
dereference ()
{
 y=\$"$1" # Имя переменной.
 echo $y # $Junk
 
 x=`eval "expr \"$y\" "`
 echo $1=$x
 eval "$1=\"Некий другой текст \"" # Присвоить новое значение.
}
Junk="Некий текст"
echo $Junk "до того как..." # Некий текст до того как...
 
dereference Junk
echo $Junk "после того как..." # Некий другой текст после того как...
 
exit 0

Пример: Ещё один пример разыменования параметров функции, передаваемых по ссылке.

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

Exit and Return
Код завершения
Функции возвращают значение в виде кода завершения. Код завершения может быть задан явно, с помощью команды return, в противном случае будет возвращен код завершения последней команды в функции (0 — в случае успеха, иначе — ненулевой код ошибки). Код завершения в сценарии может быть получен через переменную $?.
return
Завершает исполнение функции. Команда return может иметь необязательный аргумент типа integer, который возвращается в вызывающий сценарий как "код завершения" функции, это значение так же записывается в переменную $?.

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

#!/bin/bash
# max.sh: Наибольшее из двух целых чисел.
 
E_PARAM_ERR=-198 # Если функции передано меньше двух параметров.
EQUAL=-199 # Возвращаемое значение, если числа равны.
 
max2 () # Возвращает наибольшее из двух чисел.
{       # Внимание: сравниваемые числа должны быть меньше 257.
 if [ -z "$2" ]
 then
  return $E_PARAM_ERR
 fi
 if [ "$1" -eq "$2" ]
 then
  return $EQUAL
 else
  if [ "$1" -gt "$2" ]
  then
   return $1
  else
   return $2
  fi
 fi
}
 
max2 33 34
return_val=$?
 
if [ "$return_val" -eq $E_PARAM_ERR ]
then
 echo "Функции должно быть передано два аргумента."
elif [ "$return_val" -eq $EQUAL ]
 then
  echo "Числа равны."
else
 echo "Наибольшее из двух чисел: $return_val." 
fi
 
exit 0
 
# Упражнение:
# ---------------
# Сделайте этот сценарий интерактивным, 
# т.е. заставьте сценарий запрашивать числа для сравнения у пользователя (два числа).

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

count_lines_in_etc_passwd()
{
 [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
 # Если файл /etc/passwd доступен на чтение, то в переменную REPLY заносится число строк.
 # Возвращаются как количество строк, так и код завершения.
 # Команда 'echo' может показаться ненужной, но . . .
 # она предотвращает вывод лишних пробелов.
}
 
if count_lines_in_etc_passwd
then
 echo "В файле /etc/passwd найдено $REPLY строк."
else
 echo "Невозможно подсчитать число строк в файле /etc/passwd."
fi
# Спасибо S.C.

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

#!/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 Cnum=$?
num=$?
to_roman $num 90 LXXXX
num=$?
to_roman $num 50 L
num=$?
to_roman $num 40 X
Lnum=$?
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

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

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

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

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

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

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

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

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

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

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

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

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

#!/bin/bash
 
func ()
{
 local loc_var=23 # Объявление локальной переменной.
 echo
 echo "\"loc_var\" в функции = $loc_var"
 global_var=999 # Эта переменная не была объявлена локальной.
 echo "\"global_var\" в функции = $global_var"
}
 
func
# Проверим, "видна" ли локальная переменная за пределами функции.
 
echo
echo "\"loc_var\" за пределами функции = $loc_var"
                                         # "loc_var" за пределами функции =
                                         # Итак, $loc_var не видна в глобальном контексте.
echo "\"global_var\" за пределами функции = $global_var"
                                         # "global_var" за пределами функции = 999
                                         # $global_var имеет глобальную область видимости.
echo
 
exit 0

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

#!/bin/bash
 
func ()
{
global_var=37 # Эта переменная будет считаться необъявленной
              # до тех пор, пока функция не будет вызвана.
              } # КОНЕЦ ФУНКЦИИ
echo "global_var = $global_var" # global_var =
                                # Функция "func" ещё не была вызвана,
                                # поэтому $global_var пока ещё не "видна" здесь.
 
func
 
echo "global_var = $global_var" # global_var = 37
                                # Переменная была инициализирована в функции.

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

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

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

#!/bin/bash
# факториал
# ---------
 
# Действительно ли bash допускает рекурсию?
# Да! Но...
# Нужно быть действительно дубинноголовым, чтобы использовать её в сценариях
# на языке командной оболочки.
 
MAX_ARG=5
E_WRONG_ARGS=65
E_RANGE_ERR=66
 
if [ -z "$1" ]
then
 echo "Порядок использования: `basename $0` число"
 exit $E_WRONG_ARGS
fi
if [ "$1" -gt $MAX_ARG ]
then
 echo "Выход за верхний предел (максимально возможное число -- 5)."
 # Вернитесь к реальности.
 # Если вам захочется поднять верхнюю границу,
 # то перепишите эту программу на настоящем языке программирования.
 exit $E_RANGE_ERR
fi
 
fact ()
{
 local number=$1
 # Переменная "number" должна быть объявлена как локальная,
 # иначе результат будет неверный.
 if [ "$number" -eq 0 ]
 then
  factorial=1 # Факториал числа 0 = 1.
 else
  let "decrnum = number - 1"
  fact $decrnum # Рекурсивный вызов функции.
  let "factorial = $number * $?"
 fi
 return $factorial
}
 
fact $1
echo "Факториал числа $1 = $?."
 
exit 0

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

Рекурсия без локальных переменных

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

Пример: Ханойские Башни.

#! /bin/bash
#
# Ханойские Башни
# Bash script# Copyright (C) 2000 Amit Singh. All Rights Reserved.
# http://hanoi.kernelthread.com
#
# Тестировался под bash 2.05b.0(13)-release
#
# Используется в "Advanced Bash Scripting Guide"
# с разрешения автора.
# С небольшими изменениями, внесенными автором документа.
 
#=================================================================#
# Ханойские Башни — это древняя математическая головоломка.
# Дается три вертикальных стержня.
# На первый нанизан набор колец.
# Эти кольца представляют собой плоские диски с дыркой по-середине,
# так что они могут свободно скользить по стержню.
# Кольца имеют различный диаметр, и изначально расположены на первом стержне
# в порядке убывания их диаметров.
# Наименьшее кольцо расположено сверху, наибольшее — внизу.
#
# Суть задачи заключается в том, чтобы перенести кольца с первого
# стержня на последний так, чтобы порядок следования колец не изменился.
# Кольца можно перемещать со стержня на стержень только по одному за раз.
# Можно помещать кольца обратно на тот же самый стержень.
# При перемещении нельзя класть больший диск на меньший.
#
# Для небольшого количества колец требуется некоторое количество перемещений.
# Каждое дополнительное кольцо
# увеличивает необходимое количество перемещений примерно в два раза,
# при этом "стратегия" перемещения усложняется все больше и больше.
#
# За дополнительной информацией обращайтесь на http://hanoi.kernelthread.com.
#
#
#
#        ...                ...                   ...
#        | |                | |                   | |
#       _|_|_               | |                   | |
#      |_____|              | |                   | |
#     |_______|             | |                   | |
#    |_________|            | |                   | |
#   |___________|           | |                   | |
#  |             |          | |                   | |
# .--------------------------------------------------------------.
# |**************************************************************|
#        #1                 #2                    #3#
#=================================================================#
 
E_NOPARAM=66  # Сценарий запущен без параметров.
E_BADPARAM=67 # Неверное число колец.
Moves=        # Глобальная переменная, хранит число перемещений.
              # Добавлено в оригинальный сценарий.
 
dohanoi() { # Рекурсивная функция.
 case $1 in
  0)
    ;;
  *)
    dohanoi "$(($1-1))" $2 $4 $3
    echo move $2 "-->" $3
     let "Moves += 1" # Добавлено в оригинальный сценарий.
    dohanoi "$(($1-1))" $4 $3 $2
    ;;
 esac
}
 
case $# in
 1)
   case $(($1>0)) in # Как минимум должен быть хотя бы один диск.
    1)
      dohanoi $1 1 3 2
      echo "Всего перемещений = $Moves"
      exit 0;
      ;;
    *)
      echo "$0: Неверное число колец";
      exit $E_BADPARAM;
      ;;
    esac
    ;;
 *)
   echo "Порядок использования: $0 N"
   echo " Где \"N\" — это число колец."
   exit $E_NOPARAM;
   ;;
esac
# Упражнения:
# ---------
# 1) Будут ли исполнены дополнительные команды, если разместить их ниже этой строки?
# Почему нет? (Это так просто!)
# 2) Объясните — как работает функция "dohanoi".
# (А вот это уже сложнее)

Псевдонимы

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

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

#!/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 удаляет псевдоним, объявленный ранее.

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

Списки команд

Средством обработки последовательности из нескольких команд служат списки: "И-списки" и "ИЛИ-списки". Они эффективно могут заменить сложную последовательность вложенных if/then или даже case.
Объединение команд в цепочки
И-список
command-1 && command-2 && command-3 && ... command-n
Каждая последующая команда, в таком списке, выполняется только тогда, когда предыдущая команда вернула код завершения true(ноль). Если какая-либо из команд возвращает false(не ноль), то исполнение списка команд в этом месте завершается, т.е. следующие далее команды не выполняются.

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

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

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

#!/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(ноль), то исполнение cписка команд в этом месте завершается, т.е. следующие далее команды не выполняются. Очевидно, что "ИЛИ-списки" имеют смысл обратный, по отношению к "И-спискам".

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

#!/bin/bash
# delete.sh, утилита удаления файлов.
# Порядок использования: delete имя_файла
E_BADARGS=65if [ -z "$1" ]
then
 echo "Порядок использования: `basename $0` имя_файла"
 exit $E_BADARGS # Если не задано имя файла.
else
 file=$1 # Запомнить имя файла.
fi
 
[ ! -f "$file" ] && echo "Файл \"$file\" не найден. \
Робкий отказ удаления несуществующего файла."
# И-СПИСОК, выдать сообщение об ошибке, если файл не существует.
# Обратите внимание: выводимое сообщение продолжается во второй строке,
# благодаря экранированию символа перевода строки.
 
[ ! -f "$file" ] || (rm -f $file; echo "Файл \"$file\" удален.")
# ИЛИ-СПИСОК, удаляет существующий файл.
 
# Обратите внимание на логические условия.
# И-СПИСОК отрабатывает по true, ИЛИ-СПИСОК -- по false.
 
exit 0

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

false && true || echo false # false
# Тот же результат даёт( false && true ) || echo false # false
# Но не эта комбинация
false && ( true || echo false ) # (нет вывода на экран)
 
# Обратите внимание на группировку и порядок вычисления условий — слева-направо,
# поскольку логические операции "&&" и "||" имеют равный приоритет.
# Если вы не уверены в своих действиях, то лучше избегать таких сложных конструкций.
# Спасибо S.C.

Массивы

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

Пример: Простой массив.

#!/bin/bash
 
area[11]=23
area[13]=37
area[51]=UFOs
 
# Массивы не требуют, чтобы последовательность элементов в массиве была непрерывной.
# Некоторые элементы массива могут оставаться неинициализированными.
# "Дырки" в массиве не являются ошибкой.
 
echo -n "area[11] = "
echo ${area[11]} # необходимы {фигурные скобки}
 
echo -n "area[13] = "
echo ${area[13]}
 
echo "содержимое area[51] = ${area[51]}."
# Обращение к неинициализированным элементам дает пустую строку.
echo -n "area[43] = "
echo ${area[43]}
echo "(элемент area[43] — неинициализирован)"
 
echo
# Сумма двух элементов массива, записанная в третий элемент
area[5]=`expr ${area[11]} + ${area[13]}`
echo "area[5] = area[11] + area[13]"
echo -n "area[5] = "
echo ${area[5]}
 
area[6]=`expr ${area[11]} + ${area[51]}`
echo "area[6] = area[11] + area[51]"
echo -n "area[6] = "
echo ${area[6]}
# Эта попытка закончится неудачей, поскольку сложение целого числа со строкой не допускается.
 
echo; echo; echo
 
# -----------------------------------------------------------------
# Другой массив, "area2".
# И другой способ инициализации массива...
# array_name=( XXX YYY ZZZ ... )
 
area2=( ноль один два три четыре )
 
echo -n "area2[0] = "
echo ${area2[0]}
# Ага, индексация начинается с нуля (первый элемент массива имеет индекс [0], а не [1]).
 
echo -n "area2[1] = "
echo ${area2[1]} # [1] -- второй элемент массива.
# -----------------------------------------------------------------
 
echo; echo; echo
 
# -----------------------------------------------
# Ещё один массив, "area3".
# И ещё один способ инициализации...
# array_name=([xx]=XXX [yy]=YYY ...)
 
area3=([17]=семнадцать [21]=двадцать_один)
 
echo -n "area3[17] = "
echo ${area3[17]}
 
echo -n "area3[21] = "
echo ${area3[21]}
# -----------------------------------------------
 
exit 0

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

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

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

Пример: Форматирование стихотворения.

#!/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 адаптированы для работы с массивами.

Пример: Различные операции над массивами.

#!/bin/bash
# array-ops.sh: Операции над массивами.
 
array=( ноль один два три четыре пять )
 
echo ${array[0]} # ноль
echo ${array:0}  # ноль
                 # Подстановка параметра - первый элемент,
                 # начиная с позиции 0 (с 1-го символа).
echo ${array:1}  # ноль
                 # Подстановка параметра - первый элемент,
                 # начиная с позиции 1 (со 2-го символа).
 
echo "--------------"
 
echo ${#array[0]} # 4
                  # Длина первого элемента массива.
echo ${#array}    # 4
                  # Длина первого элемента массива.
                  # (Альтернативный вариант)
echo ${#array[1]} # 4
                  # Длина второго элемента массива.
                  # Индексация массивов в Bash начинается с нуля.
echo ${#array[*]} # 6
                  # Число элементов в массиве.
echo ${#array[@]} # 6
                  # Число элементов в массиве.
 
echo "--------------"
 
array2=( [0]="первый элемент" [1]="второй элемент" [3]="четвертый элемент" )
 
echo ${array2[0]} # первый элемент
echo ${array2[1]} # второй элемент
echo ${array2[2]} #
                  # Не был проинициализирован, поэтому null.
echo ${array2[3]} # четвёртый элемент
 
exit 0

Большинство стандартных операций над строками применимы к массивам.

Пример: Строковые операции и массивы.

#!/bin/bash
# array-strops.sh: Строковые операции и массивы.
# Автор: Michael Zick.
# Используется с его разрешения.
 
# Как правило, строковые операции, в нотации ${name ... }
# могут использоваться для работы со строковыми элементами массивов
# в виде: ${name[@] ... } или ${name[*] ...} .
 
arrayZ=( one two three four five five )
 
echo
# Извлечение части строки
echo ${arrayZ[@]:0}   # one two three four five five
                      # Все элементы массива.
echo ${arrayZ[@]:1}   # two three four five five
                      # Все элементы массива, начиная со 2-го.
echo ${arrayZ[@]:1:2} # two three
                      # Два элемента массива, начиная со 2-го.
echo "-----------------------"
 
# Удаление части строки
# Удаляет наименьшую из подстрок, найденных по шаблону (поиск ведётся с начала строки)
# где шаблон — это регулярное выражение.
 
echo ${arrayZ[@]#f*r} # one two three five five
                      # Находит подстроку "four" и удаляет ее.
                      # Поиск ведётся по всем элементам массива
 
# Удаляет наибольшую подстроку, из найденных по шаблону
echo ${arrayZ[@]##t*e} # one two four five five
                      # Находит подстроку "three" и удаляет её.
                      # Поиск ведётся по всем элементам массива
 
# Удаляет наименьшую из подстрок, найденных по шаблону (поиск ведётся с конца строки)
echo ${arrayZ[@]%h*e} # one two t four five five
                      # Находит подстроку "hree" и удаляет её.
                      # Поиск ведется по всем элементам массива
 
# Удаляет наибольшую из подстрок, найденных по шаблону (поиск ведётся с конца строки)
echo ${arrayZ[@]%%t*e} # one two four five five
                       # Находит подстроку "three" и удаляет её.
                       # Поиск ведётся по всем элементам массива
 
echo "-----------------------"
# Замена части строки
 
# Заменяет первую найденную подстроку заданной строкой
echo ${arrayZ[@]/fiv/XYZ} # one two three four XYZe XYZe
                          # Поиск ведётся по всем элементам массива
 
# Заменяет все найденные подстроки
echo ${arrayZ[@]//iv/YY} # one two three four fYYe fYYe
                         # Поиск ведётся по всем элементам массива
 
# Удаляет все найденные подстроки
# Если замещающая строка не задана, то это означает "удаление"
echo ${arrayZ[@]//fi/} # one two three four ve ve
                       # Поиск ведется по всем элементам массива
 
# Заменяет первую найденную подстроку (поиск ведётся с начала строки)
echo ${arrayZ[@]/#fi/XY} # one two three four XYve XYve
                         # Поиск ведётся по всем элементам массива
 
# Заменяет первую найденную подстроку (поиск ведется с конца строки)
echo ${arrayZ[@]/%ve/ZZ} # one two three four fiZZ fiZZ
                         # Поиск ведётся по всем элементам массива
 
echo ${arrayZ[@]/%o/XX} # one twXX three four five five
                        # Почему?
 
echo "-----------------------"
# Before reaching for awk (or anything else)
# Вспомним:
# $( ... ) — вызов функции.
# Функция запускается как подпроцесс.
# Функции выводят на stdout.
# Присваивание производится со stdout функции.
# Запись name[@] — означает операцию "for-each".
 
newstr() {
 echo -n "!!!"
}
 
echo ${arrayZ[@]/%e/$(newstr)}
# on!!! two thre!!! four fiv!!! fiv!!!
# Q.E.D: Замена — суть есть "присваивание".
# Доступ "For-Each" — ко всем элементам массива
echo ${arrayZ[@]//*/$(newstr optional_arguments)}
# Теперь, если бы Bash просто передал согласованную строку как $0
# к вызываемой функции...
echo
 
exit 0

Подстановка команд может создавать отдельные элементы массива.

Пример: Загрузка исходного текста сценария в массив.

#!/bin/bash
# script-array.sh: Сценарий загружает себя в массив.
# Идею подал Chris Martin (спасибо!).
 
script_contents=( $(cat "$0") ) # Записать содержимое этого сценария ($0)
                                # в массив.
 
for element in $(seq 0 $((${#script_contents[@]} - 1)))
do                          # ${#script_contents[@]}
                            # даёт число элементов массива.
                            #
                            # Вопрос:
                            # Для чего необходима команда seq 0 ?
                            # Попробуйте заменить её на seq 1.
 echo -n "${script_contents[$element]}"
                            # Вывести элементы массива в одну строку,
 echo -n " — "              # разделяя их комбинацией " — " .
done
echo
 
exit 0
 
# Упражнение:
# --------
# Попробуйте изменить сценарий таким образом,
# чтобы он выводил себя на экран в первоначальном виде,
# со всеми пробелами, переводами строк и т.п.

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

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

#!/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]}.

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

#!/bin/bash
# empty-array.sh
# Выражаю свою благодарность Stephane Chazelas за этот пример,
# и Michael Zick за его доработку.
 
# Пустой массив — это не то же самое, что массив с пустыми элементами.
array0=( первый второй третий )
array1=( '' ) # "array1" имеет один пустой элемент.
array2=( )    # Массив "array2" не имеет ни одного элемента, т.е. пуст.
 
echo
ListArray()
{
echo
echo "Элементы массива array0: ${array0[@]}"
echo "Элементы массива array1: ${array1[@]}"
echo "Элементы массива array2: ${array2[@]}"
echo
echo "Длина первого элемента массива array0 = ${#array0}"
echo "Длина первого элемента массива array1 = ${#array1}"
echo "Длина первого элемента массива array2 = ${#array2}"
echo
echo "Число элементов в массиве array0 = ${#array0[*]}" # 3
echo "Число элементов в массиве array1 = ${#array1[*]}" # 1 (сюрприз!)
echo "Число элементов в массиве array2 = ${#array2[*]}" # 0
}
 
# ===================================================================
ListArray
# Попробуем добавить новые элементы в массивы
 
# Добавление новых элементов в массивы.
array0=( "${array0[@]}" "новый1" )
array1=( "${array1[@]}" "новый1" )
array2=( "${array2[@]}" "новый1" )
 
ListArray
 
# или
array0[${#array0[*]}]="новый2"
array1[${#array1[*]}]="новый2"
array2[${#array2[*]}]="новый2"
 
ListArray
# Теперь представим каждый массив как 'стек' ('stack')
# Команды выше, можно считать командами 'push' — добавление нового значения на вершину стека
# 'Глубина' стека:
height=${#array2[@]}
echo
echo "Глубина стека array2 = $height"
 
# Команда 'pop' — выталкивание элемента стека, находящегося на вершине:
unset array2[${#array2[@]}-1] # Индексация массивов начинается с нуля
height=${#array2[@]}
echo
echo "POP"
echo "Глубина стека array2, после выталкивания = $height"
 
ListArray
# Вывести только 2-й и 3-й элементы массива array0
from=1 # Индексация массивов начинается с нуля
to=2   #
declare -a array3=( ${array0[@]:1:2} )
echo
echo "Элементы массива array3: ${array3[@]}"
 
# Замена элементов по шаблону
declare -a array4=( ${array0[@]/второй/2-й} )
echo
echo "Элементы массива array4: ${array4[@]}"
 
# Замена строк по шаблону
declare -a array5=( ${array0[@]//новый?/старый} )
echo
echo "Элементы массива array5: ${array5[@]}"
 
# Надо лишь привыкнуть к такой записи...
declare -a array6=( ${array0[@]#*новый} )
echo # Это может вас несколько удивить
echo "Элементы массива array6: ${array6[@]}"
 
declare -a array7=( ${array0[@]#новый1} )
echo # Теперь это вас уже не должно удивлять
echo "Элементы массива array7: ${array7[@]}"
 
# Выглядит очень похоже на предыдущий вариант...
declare -a array8=( ${array0[@]/новый1/} )
echo
echo "Элементы массива array8: ${array8[@]}"
 
# Итак, что вы можете сказать обо всем этом?
 
# Строковые операции выполняются последовательно, над каждым элементом в массиве var[@].
# Таким образом, BASH поддерживает векторные операции
# Если в результате операции получается пустая строка, то элемент массива "исчезает".
 
# Вопрос: это относится к строкам в "строгих" или "мягких" кавычках?
 
zap='новый*'
declare -a array9=( ${array0[@]/$zap/} )
echo
echo "Элементы массива array9: ${array9[@]}"
 
# "...А с платформы говорят: "Это город Ленинград!"..."
declare -a array10=( ${array0[@]#$zap} )
echo
echo "Элементы массива array10: ${array10[@]}"
 
# Сравните массивы array7 и array10
# Сравните массивы array8 и array9
# Ответ: в "мягких" кавычках.
 
exit 0

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

# Копирование массивов.
array2=( "${array1[@]}" )
# или
array2="${array1[@]}"
 
# Добавить элемент.
array=( "${array[@]}" "новый элемент" )
# или
array[${#array[*]}]="новый элемент"
# Спасибо S.C.

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

#!/bin/bash
filename=sample_file
#           cat sample_file
#
#           1 a b c
#           2 d e fg
 
declare -a array1array1=( `cat "$filename"`) # Загрузка содержимого файла $filename в массив array1.
 
# Вывод на stdout.
#
# array1=( `cat "$filename" | tr '\n' ' '`)
#                         с заменой символов перевода строки на пробелы.
# Впрочем, в этом нет необходимости, поскольку Bash
# выполняет разбивку по словам заменяя символы перевода строки
# на пробелы автоматически.
 
echo ${array1[@]} # список элементов массива.
#           1 a b c 2 d e fg
#
# Каждое "слово", в текстовом файле, отделяемое от других пробелами
# заносится в отдельный элемент массива.
 
element_count=${#array1[*]}
echo $element_count # 8

Пример: Инициализация массивов.

#! /bin/bash
# array-assign.bash
# Поскольку здесь рассматриваются операции, специфичные для Bash,
# файл сценария имеет расширение ".bash".
# Copyright (c) Michael S. Zick, 2003, All rights reserved.
# Лицензионное соглашение: Допускается использование сценария# в любом виде без каких либо ограничений.
# Версия: $ID$
 
# Основан на примере, предоставленном Stephane Chazelas,
# который включен в состав книги: Advanced Bash Scripting Guide.
 
# Формат вывода команды 'times':
# User CPU <space> System CPU
# User CPU of dead children <space> System CPU of dead children
 
# Bash предоставляет два способа записи всех элементов
# одного массива в другой.
# В Bash, версий 2.04, 2.05a и 2.05b,
# оба они пропускают 'пустые' элементы
# В более новых версиях добавлена возможность присваивания
# в виде [индекс]=значение.
 
declare -a bigOne=( /dev/* )
echo
echo 'Условия проверки: Отсутствие кавычек, IFS по-умолчанию, Все-Элементы'
echo "Количество элементов в массиве: ${#bigOne[@]}"
 
# set -vx
echo
echo '- - проверяется: =( ${array[@]} ) - -'
times
declare -a bigTwo=( ${bigOne[@]} )
#                 ^              ^
times
echo
echo '- - проверяется: =${array[@]} - -'
times
declare -a bigThree=${bigOne[@]}
# Обратите внимание: круглые скобки отсутствуют.
times
# Сравнение временных показателей свидетельствует о том, что вторая форма записи,
# как заметил Stephane Chazelas, работает в 3-4 раза быстрее.
# Тем не менее, в своих примерах, я буду продолжать использовать первую форму записи
# потому что, на мой взгляд, она более показательна.
 
# Однако, в отдельных случаях, я буду использовать вторую форму записи,
# с целью увеличения скорости исполнения сценариев.
# MSZ: Прошу прощения, что не предупредил об этом заранее!
 
exit 0

Явное объявление массива с помощью конструкции declare -a может повысить скорость работы с этим массивом в последующих операциях.

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

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

Пример: Ещё пример на конкатенацию массивов.

#! /bin/bash
# array-append.bash
# Copyright (c) Michael S. Zick, 2003, All rights reserved.
# Лицензионное соглашение: Допускается использование сценария
# в любом виде без каких-либо ограничений.
# Версия: $ID$
#
# С небольшими изменениями, внесёнными автором книги.
 
# Действия над массивами являются специфичными для Bash.
# Эквиваленты в /bin/sh отсутствуют!
 
# Чтобы избежать скроллинга выводимой информации за пределы терминала,
# передайте вывод от сценария, через конвейер, команде 'more'.
# Упакованный массив.
declare -a array1=( zero1 one1 two1 )
# Разреженный массив (элемент [1] -- не определен).
declare -a array2=( [0]=zero2 [2]=two2 [3]=three2 )
 
echo
echo '- Проверка того, что массив получился разреженным. -'
echo "Число элементов: 4" # Жёстко "зашито", в демонстрационных целях.
for (( i = 0 ; i < 4 ; i++ ))
do
 echo "Элемент [$i]: ${array2[$i]}"
done
# См. так же пример basics-reviewed.bash.
 
declare -a dest
# Конкатенация двух массивов.
echo
echo 'Условия: Отсутствие кавычек, IFS по-умолчанию, Все-Элементы'
echo '- Неопределённые элементы не передаются. -'
#
# На самом деле неопределенные элементы отсутствуют в массиве.
dest=( ${array1[@]} ${array2[@]} )
# dest=${array1[@]}${array2[@]} # Странный результат, возможно ошибка.
 
# Теперь выведем результат.
echo
echo '- - Проверка конкатенации массивов - -'
cnt=${#dest[@]}
 
echo "Число элементов: $cnt"
for (( i = 0 ; i < cnt ; i++ ))
do
 echo "Элемент [$i]: ${dest[$i]}"
done
# Записать массив в элемент другого массива (дважды).
dest[0]=${array1[@]}
dest[1]=${array2[@]}
 
# Вывести результат.
echo
echo '- - Проверка записи содержимого одного массива в элемент другого массива - -'
cnt=${#dest[@]}
 
echo "Число элементов: $cnt"
for (( i = 0 ; i < cnt ; i++ ))
do
 echo "Элемент [$i]: ${dest[$i]}"
done
 
# Рассмотрение содержимого второго элемента.
echo
echo '- - Запись содержимого второго элемента и вывод результата - -'
 
declare -a subArray=${dest[1]}
cnt=${#subArray[@]}
echo "Число элементов: $cnt"
for (( i = 0 ; i < cnt ; i++ ))
do
 echo "Элемент [$i]: ${subArray[$i]}"
done
# Запись содержимого целого массива в элемент другого массива,
# с помощью конструкции '=${ ... }',
# приводит к преобразованию содержимого первого массива в строку,
# в которой отдельные элементы первого массива разделены пробелом
# (первый символ из переменной IFS).
 
# If the original elements didn't contain whitespace . . .
# If the original array isn't subscript sparse . . .
# Then we could get the original array structure back again.
 
# Restore from the modified second element.
echo
echo '- - Listing restored element - -'
 
declare -a subArray=( ${dest[1]} )
cnt=${#subArray[@]}
echo "Number of elements: $cnt"
for (( i = 0 ; i < cnt ; i++ ))
do
 echo "Element [$i]: ${subArray[$i]}"
done
echo '- - Не зависеть от этого поведения. - -'
echo '- - Такое поведение может быть изменено - -'
echo '- - в версиях Bash, более новых, чем версия 2.05b - -'
 
# MSZ: Прошу прощения за любую раннюю путаницу, ребята.
 
exit 0

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

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

#!/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.
# незначительные изменения и комментарии добавил William Park.
 
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} )
# Этот массив содержит шесть элементов:
# SubArray=( [0]=${AnArray[11]} [1]=${AnArray[6]} [2]=${AnArray[7]}
# [3]=${AnArray[8]} [4]=${AnArray[9]} [5]=${AnArray[10]} )
#
# Массивы в Bash оформляются в виде связанных (циклических) списков
# где каждый элемент списка имеет тип string (char *).
# Таким образом, вложенные массивы фактически таковыми не являются,
# хотя функционально очень похожи на них.
 
echo "Текущий каталог и дата последнего изменения:"
echo "${SubArray[@]}"
exit 0

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

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

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

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

#!/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
printprimes
 
# Это называется структурным программированием.
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

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

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

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

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

#!/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 поддерживает только одномерные массивы, но, путем небольших ухищрений, можно эмулировать многомерные массивы.

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

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

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

Файлы

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

/etc/profile
Настройки системы по-умолчанию, главным образом настраивается окружение командной оболочки (все Bourne-подобные оболочки, не только Bash)
/etc/bashrc
функции и псевдонимы Bash
$HOME/.bash_profile
пользовательские настройки окружения Bash, находится в домашнем каталоге у каждого пользователя (локальная копия файла /etc/profile)
$HOME/.bashrc
пользовательский файл инициализации Bash, находится в домашнем каталоге у каждого пользователя (локальная копия файла /etc/bashrc).

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

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

/dev и /proc

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

/dev

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

bash$ df
Filesystem          1k-blocks         Used        Available       Use%       Mounted on
/dev/hda6           495876            222748 2    47527           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. С помощью такого устройства можно представить обычный файл, как блочное устройство ввода/вывода. Это позволяет монтировать целые файловые системы, находящиеся в отдельных больших файлах.
Отдельные псевдоустройства в /dev имеют особое назначение, к таким устройствам можно отнести /dev/null, /dev/zero, /dev/urandom и /dev/tcp.

Пример: Пример работы с псевдоустройством /dev/tcp.

#!/bin/bash
# dev-tcp.sh: Пример перенаправления на псевдоустройство /dev/tcp
 
# Автор: Troy Engel.
# Используется с разрешения автора.
 
TCP_HOST=www.slashdot.org
TCP_PORT=80 # 80-й порт — это стандартный порт службы http.
 
# Попытка выполнить соединение. (Своего рода 'ping.')
echo "HEAD / HTTP/1.0" >/dev/tcp/${TCP_HOST}/${TCP_PORT}
MYEXIT=$?
 
: << EXPLANATION
Если bash собран с ключом --enable-net-redirections, то он имеет возможность использовать специальное символьное устройство для перенаправления TCP и UDP.
Оно практически идентично стандартным устройствам STDIN/STDOUT/STDERR.
Создать это устройство (если его ещё нет в системе) можно командой(разумеется, что при этом вы должны обладать правами root):
mknod /dev/tcp c 30 36
>Из руководства к bash:
/dev/tcp/host/port
 Если host является верным именем сетевого узла или IP-адресом, а port — целое число
или название сетевой службы, то Bash попытается открыть TCP-соединение с соответствующим узлом.
EXPLANATION
 
if [ "X$MYEXIT" = "X0" ]; then
 echo "Соединение установлено. Код завершения: $MYEXIT"
else
 echo "Ошибка при попытке установить соединение. Код завершения: $MYEXIT"
fi
 
exit $MYEXIT

/proc

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

bash$ cat /proc/devices
Character devices:
1 mem
2 pty
3 ttyp
4 ttyS
5 cua
7 vcs
10 misc
14 sound
29 fb
36 netlink
128 ptm
136 pts
162 raw
254 pcmcia
 
Block devices:
1 ramdisk
2 fd
3 ide0
9 md
 
bash$ cat /proc/interrupts
CPU0
0: 84505 XT-PIC timer
1: 3375 XT-PIC keyboard
2: 0 XT-PIC cascade
5: 1 XT-PIC soundblaster
8: 1 XT-PIC rtc
12: 4231 XT-PIC PS/2 Mouse
14: 109373 XT-PIC ide0
NMI: 0
ERR: 0
bash$ cat /proc/partitions
major   minor    #blocks    name    rio    rmerge    rsect    ruse    wio    wmerge    wsect    wuse    running    use     aveq
3       0        3007872    hda     4472   22260     114520   94240   3551   18703     50384    549710  0          111550  644030
3       1        52416      hda1    27     395       844      960     4      2         14       180     0          800     1140
3       2        1          hda2    0      0         0        0       0      0         0        0       0          0       0
3       4        165280     hda4    10     0         20       210     0      0         0        0       0          210     210
...
 
bash$ cat /proc/loadavg
0.13 0.42 0.27 2/44 1119

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

bash$ cat /proc/filesystems | grep iso9660
   iso9660
 
 
 
kernel_version=$( awk '{ print $3 }' /proc/version )
 
 
CPU=$( awk '/model name/ {print $4}' < /proc/cpuinfo )
if [ $CPU = Pentium ]
then
 выполнить_ряд_специфичных_команд
 ...
else
 выполнить_ряд_других_специфичных_команд
 ...
fi

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

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

#!/bin/bash
# pid-identifier.sh: Возвращает полный путь к исполняемому файлу программы по идентификатору процесса (pid).
 
ARGNO=1 # Число, ожидаемых из командной строки, аргументов.
E_WRONGARGS=65
E_BADPID=66
E_NOSUCHPROCESS=67
E_NOPERMISSION=68
PROCFILE=exe
 
if [ $# -ne $ARGNO ]
then
 echo "Порядок использования: `basename $0` PID-процесса" >&2 # Сообщение об ошибке на >stderr.
 exit $E_WRONGARGS
fi
 
ps ax
 
pidno=$( ps ax | grep $1 | awk '{ print $1 }' | grep $1 )
# Проверка наличия процесса с заданным pid в списке, выданном командой "ps", поле #1.
# Затем следует убедиться, что этот процесс не был запущен этим сценарием ('ps').
# Это делает последний "grep $1".
 
if [ -z "$pidno" ] # Если после фильтрации получается пустая строка,
then # то это означает, что в системе нет процесса с заданным pid.
 echo "Нет такого процесса."
 exit $E_NOSUCHPROCESS
fi
 
# Альтернативный вариант:
#if ! ps $1 > /dev/null 2>&1
# then 
# в системе нет процесса с заданным pid.
# echo "Нет такого процесса."
# exit $E_NOSUCHPROCESS
# fi
 
if [ ! -r "/proc/$1/$PROCFILE" ] # Проверить право на чтение.
then
 echo "Процесс $1 найден, однако..."
 echo "у вас нет права на чтение файла /proc/$1/$PROCFILE."
 exit $E_NOPERMISSION # Обычный пользователь не имеет прав
                      # на доступ к некоторым файлам в каталоге /proc.
fi
# Последние две проверки могут быть заменены на:
# if ! kill -0 $1 > /dev/null 2>&1 # '0' — это не сигнал, но
                                   # команда всё равно проверит наличие
                                   # процесса-получателя.
# then echo "Процесс с данным PID не найден, либо вы не являетесь его владельцем" >&2
# exit $E_BADPID
# fiexe_file=$( ls -l /proc/$1 | grep "exe" | awk '{ print $11 }' )
 
exe_file=$( ls -l /proc/$1 | grep "exe" | awk '{ print $11 }' )
# Или exe_file=$( ls -l /proc/$1/exe | awk '{print $11}' )
#
#/proc/pid-number/exe — это символическая ссылка
# на исполняемый файл работающей программы.
 
if [ -e "$exe_file" ] # Если файл /proc/pid-number/exe существует...
then # то существует и соответствующий процесс.
 echo "Исполняемый файл процесса #$1: $exe_file."
else
 echo "Нет такого процесса."
fi
# В большинстве случаев, этот, довольно сложный сценарий, может быть заменен командой
# ps ax | grep $1 | awk '{ print $5 }'
# В большинстве, но не всегда...
# поскольку пятое поле листинга,выдаваемого командой 'ps', это argv[0] процесса,
# а не путь к исполняемому файлу.
#
# Однако, оба следующих варианта должны работать безотказно.
# find /proc/$1/exe -printf '%l\n'
# lsof -aFn -p $1 -d txt | sed -ne 's/^n//p'
 
# Автор последнего комментария: Stephane Chazelas.
 
exit 0

Пример: Проверка состояния соединения.

#!/bin/bash
PROCNAME=pppd # демон ppp
PROCFILENAME=status # Что смотреть.
NOTCONNECTED=65
INTERVAL=2 # Период проверки — раз в 2 секунды.
 
pidno=$( ps ax | grep -v "ps ax" | grep -v grep | grep $PROCNAME | awk '{ print $1 }' )
# Найти идентификатор процесса 'pppd', 'ppp daemon'.
# По пути убрать из листинга записи о процессах, порожденных сценарием.
#
# Однако, как отмечает Oleg Philon,
# Эта последовательность команд может быть заменена командой "pidof".
# pidno=$( pidof $PROCNAME )
#
# Мораль:
# Когда последовательность команд становится слишком сложной,
# это повод к тому, чтобы поискать более короткий вариант.
 
if [ -z "$pidno" ] # Если получилась пустая строка, значит процесс не запущен.
then
 echo "Соединение не установлено."
 exit $NOTCONNECTED
else
 echo "Соединение установлено."; echo
fi
 
while [ true ] # Бесконечный цикл.
do
 if [ ! -e "/proc/$pidno/$PROCFILENAME" ]
 # Пока работает процесс, файл "status" существует.
 then
  echo "Соединение разорвано."
  exit $NOTCONNECTED
 fi
 netstat -s | grep "packets received" # Получить некоторые сведения о соединении.
 netstat -s | grep "packets delivered"
 
 sleep $INTERVAL
 echo; echo
done
 
exit 0
# Как обычно, этот сценарий может быть остановлен комбинацией клавиш Control-C.
 
# Упражнение:
# ----------
# Добавьте возможность завершения работы сценария, по нажатии на клавишу "q".
# Это сделает скрипт более дружественным к пользователю.

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

/dev/zero и /dev/null

/dev/null

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

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

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

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

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

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

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

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

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

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

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

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

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

Пример: Создание файла подкачки (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-устройства или для безопасного удаления файла.

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

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

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

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

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

#!/bin/bash
# ex74.sh
# Этот сценарий содержит ошибку.
a=37
if [$a -gt 27 ]
then
 echo $a
fi
exit 0

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

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

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

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

#!/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 точно установил наличие ошибочной ситуации. Сообщения об ошибках могут вообще не содержать номера строки, при исполнении которой эта ошибка появилась. А что делать, если сценарий работает, но не так как ожидалось? Вот пример весьма распространенной логической ошибки.

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

Пример: Проверка условия с помощью функции "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, по которому процесс завершает работу, т.е. — сам сценарий. Часто бывает полезным по выходу из сценария выдать "распечатку" переменных.
Установка ловушек на сигналы
trap
Определяет действие при получении сигнала; так же полезна при отладке.
Сигнал (signal) — это просто сообщение, передаётся процессу либо ядром, либо другим процессом, чтобы побудить процесс выполнить какие-либо действия(обычно — завершить работу). Например, нажатие на Control-C, вызывает передачу сигнала SIGINT, исполняющейся программе.

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

Пример: Ловушка на выходе.

#!/bin/bash
trap 'echo Список переменных — a = $a b = $b' EXIT
# EXIT — это название сигнала, генерируемого при выходе из сценария.
 
a=39
b=36
exit 0
# Примечательно, что если закомментировать команду 'exit',
# то это никак не скажется на работе сценария,
# поскольку "выход" из сценария происходит в любом случае.

Пример: Удаление временного файла при нажатии на 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"
# Упражнение: Найдите сильные и слабые стороны
# каждого из этих подходов.

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

Пример: Трассировка переменной.

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

Конструкция trap ' ' SIGNAL (две одиночных кавычки) — запрещает SIGNAL для оставшейся части сценария. Конструкция trap SIGNAL — восстанавливает действие сигнала SIGNAL. Эти конструкции могут использоваться для защиты критических участков сценария от нежелательного прерывания.

trap '' 2 # Сигнал 2 (Control-C) — запрещён.
command
command
command
trap 2 # Разрешение реакции на Control-C

Необязательные параметры(ключи)

Необязательные параметры — это дополнительные ключи (опции), которые оказывают влияние на поведение сценария и/или командной оболочки. Команда set позволяет задавать дополнительные опции прямо внутри сценария. В том месте сценария, где необходимо, чтобы та или иная опция вступила в силу, вставьте такую конструкцию set -o option-name, или в более короткой форме — set -option-abbrev. Эти две формы записи совершенно идентичны по своему действию.

#!/bin/bash
 
set -o verbose
# Вывод команд перед их исполнением.

#!/bin/bash
 
set -v
# Имеет тот же эффект, что и выше.

Для того, чтобы отключить действие той или иной опции, следует вставить конструкцию set +ooption-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

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

Таблица Ключи Bash

Краткое имя Полное имя Описание
-C noclobber Предотвращает перезапись файла в операциях перенаправления вывода (не распространяется на конвейеры (каналы) -->|)
-D (нет) Выводит список строк в двойных кавычках, которым предшествует символ $, сам сценарий не исполняется
-a allexport Экспорт всех, определенных в сценарии, переменных
-b notify Выводит уведомление по завершении фоновой задачи (job) (довольно редко используется в сценариях)
-c ... (нет) Читает команды из ...
-e errexit Прерывает исполнения сценария при появлении первой же ошибки, т.е. когда встретится команда, которая

вернёт ненулевой код возврата(за исключением циклов until или while, оператора if, цепочек из команд)

-f noglob Подстановка имён файлов(globbing) запрещена
-i interactive Сценарий запускается в интерактивном режиме
-n noexec Команды в сценарии считываются, но не исполняются(проверка синтаксиса)
-o Option-Name (нет) Установка опции Option-Name option
-o posix POSIX Изменяет поведение Bash или сценария таким образом, чтобы оно соответствовало стандарту POSIX.
-p privileged Сценарий запускается как "suid"(осторожно!)
-r restricted Сценарий запускается в ограниченном режиме
-s stdin Читает команды с устройства стандартного ввода stdin
-t (нет) Завершает работу после выполнения первой команды
-u nounset При попытке обращения к неопределённым переменным, выдаёт сообщение об

ошибке и прерывает работу сценария

-v verbose Выводит на stdout каждую команду прежде, чем она будет исполнена
-x xtrace Подобна -v, но выполняет подстановку команд
- (нет) Конец списка ключей(опций), последующие аргументы будут восприниматься как позиционные параметры.
-- (нет) Эквивалент предыдущей опции(-).

Широко распространённые ошибки

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: Попытка строкового сравнения для целых чисел.
 
echo
number=1
# Следующий цикл "while" порождает две ошибки:
# одна обнаруживается сразу, другая не так очевидна.
 
while [ "$number" < 5 ] # Ошибка! Должно быть: while [ "$number" -lt 5 ]
do
 echo -n "$number "
 let "number += 1"
done
# При попытке запустить этот сценарий на терминал выводится сообщение:
# bad-op.sh: line 10: 5: No such file or directory
# Внутри одиночных квадратных скобок, символ "<" должен экранироваться,
# но даже если соблюсти синтаксис, то результат сравнения все равно будет неверным.
echo "---------------------"
while [ "$number" \< 5 ]      # 1 2 3 4
do                            #
 echo -n "$number "           # Здесь вроде бы нет ошибки, но . . .
 let "number += 1"            # фактически выполняется сравнение строк,
done                          # а не чисел.
 
echo; echo "---------------------"
# Это может породить определённые проблемы, например:
 
lesser=5
greater=105
 
if [ "$greater" \< "$lesser" ]
then
 echo "число $greater меньше чем число $lesser"
fi                             # число 105 меньше чем число 5
# И действительно! Строка "105" меньше чем строка "5"!
# (при выполнении сравнения ASCII кодов).
 
echo
exit 0

Иногда, в операциях проверки, с использованием квадратных скобок ([ ]), переменные необходимо брать в двойные кавычки(" ").
Иногда сценарий не в состоянии выполнить команду из-за нехватки прав доступа. Если пользователь не сможет запустить команду из командной строки, то эта команда не сможет быть запущена и из сценария. Попробуйте изменить атрибуты команды, возможно вам придется установить бит 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-систем в целом. Использование недокументированных возможностей Bash весьма небезопасная практика. Предыдущие версии этой книги включали в себя ряд сценариев , которые использовали такие "возможности", например — возможность возвращать через exit или return большие (по абсолютному значению) отрицательные целые числа. К сожалению, в версии 2.05b и более поздних, эта "лазейка" была закрыта.
\ Сценарий, в котором строки отделяются друг от друга в стиле 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 останется неинициализированной. Использование в подоболочке переменных с теми же именами, что и в родительской оболочке может не давать ожидаемого результата.

Пример: Западня в подоболочке.

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

Пример: Передача вывода от команды 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

Фактически, как указывает Anthony Richardson, передача вывода по конвейеру в любой цикл, может порождать аналогичные проблемы.

# Проблемы с передачей данных в цикл по конвейеру.
# Этот пример любезно предоставил Anthony Richardson.
 
foundone=false
find $HOME -type f -atime +30 -size 100k |
while true
do
 read f
 echo "Файл $f имеет размер более 100KB и не использовался более 30 дней"
 echo "Подумайте о перемещении этого файла в архив."
 foundone=true
done
 
# Переменная foundone всегда будет иметь значение false, поскольку
# она устанавливается в пределах подоболочки
 
if [ $foundone = false ]
then
 echo "Не найдено файлов, которые требуют архивации."
fi
 
# =====================А теперь правильный вариант:=================
foundone=false
for f in $(find $HOME -type f -atime +30 -size 100k) # Здесь нет конвейера.
do
 echo "Файл $f имеет размер более 100KB и не использовался более 30 дней"
 echo "Подумайте о перемещении этого файла в архив."
 foundone=true
done
 
if [ $foundone = false ]
then
 echo "Не найдено файлов, которые требуют архивации."
fi

Подобные же проблемы возникают при попытке записать вывод от tail -f в конвейере с grep.

tail -f /var/log/messages | grep "$ERROR_MSG" >> error.log
# Ни одна запись не попадёт в файл "error.log".

Огромный риск, для безопасности системы, представляет использование в скриптах команд, с установленным битом "suid". Использование сценариев в качестве 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

Стиль программирования

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

Неофициальные рекомендации по оформлению сценариев

  • Комментируйте свой код. Это сделает ваши сценарии понятнее для других, и более простыми, в обслуживании, для вас.
PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}"
# Эта строка имела некоторый смысл в момент написания,
# но через год-другой будет очень тяжело вспомнить — что она делает.
# (Из сценария "pw.sh", автор: Antek Sawicki)

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

#!/bin/bash
#************************************************#
#                  xyz.sh                        #
#           автор: Bozo Bozeman                  #
#                Июль 05, 2001                   #
#                                                #
#           Удаление файлов проекта.             #
#************************************************#
BADDIR=65                      # Нет такого каталога.
projectdir=/home/bozo/projects # Каталог проекта.
 
# ------------------------------------------------------- #
# cleanup_pfiles ()                                       #
# Удаляет все файлы в заданном каталоге.                  #
# Параметры: $target_directory                            #
# Возвращаемое значение: 0 — в случае успеха,             #
# $BADDIR — в случае ошибки.                              #
# ------------------------------------------------------- #
 
cleanup_pfiles ()
{
if [ ! -d "$1" ] # Проверка существования заданного каталога.
then
 echo "$1 — не является каталогом."
 return $BADDIR
fi
 
 rm -f "$1"/*
 return 0 # Успешное завершение функции.
}
 
cleanup_pfiles $projectdir
 
exit 0

Не забывайте начинать ваш сценарий с sha-bang#!/bin/bash.

  • Заменяйте повторяющиеся значения константами. Это сделает ваш сценарий более простым для понимания и позволит вносить изменения, не опасаясь за его работоспособность.
if [ -f /var/log/messages ]
then
 ...
fi
# Представьте себе, что через пару лет
# вы захотите изменить /var/log/messages на /var/log/syslog.
# Тогда вам придётся отыскать все строки,
# содержащие /var/log/messages, и заменить их на /var/log/syslog.
# И проверить несколько раз — не пропустили ли что-нибудь.
 
# Использование "констант" даёт лучший способ:
LOGFILE=/var/log/messages # Если и придётся изменить, то только в этой строке.
if [ -f "$LOGFILE" ]
then
 ...
fi

  • В качестве имён переменных и функций выбирайте осмысленные названия.
fl=`ls -al $dirname`             # Не очень удачное имя переменной.
file_listing=`ls -al $dirname`   # Уже лучше.
 
MAXVAL=10                        # Пишите имена констант в верхнем регистре.
while [ "$index" -le "$MAXVAL" ]
...
 
E_NOTFOUND=75                    # Имена кодов ошибок — в верхнем регистре,
                                 # к тому же, их желательно дополнять префиксом "E_".
if [ ! -e "$filename" ]
then
 echo "Файл $filename не найден."
 exit $E_NOTFOUND
fi
 
MAIL_DIRECTORY=/var/spool/mail/bozo # Имена переменных окружения
                                    # так же желательно записывать символами
                                    # в верхнем регистре.
export MAIL_DIRECTORY
 
GetAnswer ()                        # Смешивание символов верхнего и нижнего регистров
                                    # удобно использовать для имён функций.
{
 prompt=$1
 echo -n $prompt
 read answer
 return $answer
}
 
GetAnswer "Ваше любимое число? "
 
favorite_number=$?
echo $favorite_number_user
 
variable=23                         # Допустимо, но не рекомендуется.
# Желательно, чтобы пользовательские переменные не начинались с символа подчёркивания.
# Так обычно начинаются системные переменные.

  • Используйте смысловые имена для кодов завершения.
E_WRONG_ARGS=65
...
...
exit $E_WRONG_ARGS

  • Разделяйте большие сложные сценарии на серию более коротких и простых модулей. Пользуйтесь функциями.

  • Не пользуйтесь сложными конструкциями, если их можно заменить простыми.
COMMAND
if [ $? -eq 0 ]
...
# Избыточно и неинтуитивно.
if COMMAND...
# Более понятно и коротко.

... читая исходные тексты сценариев на Bourne
shell (/bin/sh). Я был потрясен тем, насколько
непонятно и загадочно могут выглядеть очень
простые алгоритмы из-за неправильного
оформления кода. Я не раз спрашивал себя:
"Неужели кто-то может гордиться таким кодом?"
Landon Noll

Разное

Практически никто не знает грамматики Bourne
shell-а. Даже изучение исходных текстов не даёт
её полного понимания.
Tom Duff

Интерактивный и неинтерактивный режим работы

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

#!/bin/bash
MY_PROMPT='$ '
while :
do
 echo -n "$MY_PROMPT"
 read line
 eval "$line"
done
exit 0
# Этот сценарий, как иллюстрация к вышесказанному, предоставлен Stephane Chazelas (спасибо).

Будем считать интерактивным такой сценарий, который может принимать ввод от пользователя, обычно с помощью команды read. В "реальной жизни" все намного сложнее. Пока же, будем придерживаться предположения о том, что интерактивный сценарий ограничен рамками tty, с которого сценарий был запущен пользователемa, т.е консоль или окно xterm. Сценарии начальной инициализации системы не являются интерактивными, поскольку они не предполагают вмешательство человека в процессе своей работы. Большая часть сценариев, выполняющих администрирование и обслуживание системы — так же работают в неинтерактивном режиме. Многие задачи автоматизации труда администратора очень трудно представить себе без неинтерактивных сценариев. Неинтерактивные сценарии прекрасно могут работать в фоне, в то время, как интерактивные — подвисают, останавливаясь на операциях, ожидающих ввода пользователя. Сложности, возникающие с запуском интерактивных сценариев в фоновом режиме, могут быть преодолены с помощью expect-сценария или встроенного документа. В простейших случаях, можно организовать перенаправление ввода из файла в команду read (read variable <file). Эти приёмы позволят создавать сценарии, которые смогут работать как в интерактивном, так и в неинтерактивном режимах.
Если внутри сценария необходимо проверить режим работы — интерактивный или неинтерактивный, это можно сделать проверкой переменной окружения $PS1.

if [ -z $PS1 ] # интерактивный режим?
then
 # неинтерактивный
 ...
else
 # интерактивный
 ...
fi

Ещё один способ — проверка установки флага "i" в переменной $-.

case $- in
 *i*) # интерактивный режим
 ;;
 *) # неинтерактивный режим
 ;;
# (Из "Unix F.A.Q.," 1993)

Сценарий может принудительно запускаться в интерактивном режиме, для этого необходимо указать ключ -i в строке-заголовке #!/bin/bash -i. Однако вы должны помнить о том, что в таких случаях сценарий может выдавать сообщения об ошибках даже тогда, когда ошибок, по сути, нет.

Сценарии-обёртки

"Обёртки" — это сценарии, которые содержат один или несколько вызовов системных команд или утилит, с длинным списком параметров. Такой приём освобождает пользователя от необходимости вводить вручную сложные и длинные команды из командной строки. Он особенно полезен при работе с sed и awk.
Сценарии sed или awk, как правило вызываются в форме: sed -e 'commands' или awk 'commands' . "Заворачивая" такие вызовы в сценарий на языке командной оболочки, мы делаем их использование более простым для конечного пользователя. Кроме того, этот приём позволяет комбинировать вызовы sed и awk, например в конвейере, позволяя передавать данные с выхода одной утилиты на вход другой.

Пример: Сценарий-обёртка.

#!/bin/bash
 
# Этот простой сценарий удаляет пустые строки из текстового файла.
# Проверка входных аргументов не производится.
#
# Однако вы можете дополнить сценарий такой проверкой,
# добавив нечто подобное:
# if [ -z "$1" ]
# then
# echo "Порядок использования: `basename $0` текстовый_файл"
# exit 65
# fi
# Для выполнения этих же действий,
# из командной строки можно набрать
# sed -e '/^$/d' filename
 
sed -e /^$/d "$1"
# '-e' — означает команду "editing" (правка), за которой следуют необязательные параметры.
# '^' — с начала строки, '$' — до её конца.
# Что соответствует строкам, которые не содержат символов между началом и концом строки,
# т.е. — пустым строкам.
# 'd' — команда "delete" (удалить).
 
# Использование кавычек даёт возможность
# обрабатывать файлы, чьи имена содержат пробелы.
 
exit 0

Пример: Более сложный пример сценария-обёртки.

#!/bin/bash
# "subst", Сценарий замены по шаблону
# т.е., "subst Smith Jones letter.txt".
 
ARGS=3
E_BADARGS=65 # Неверное число аргументов.
 
if [ $# -ne "$ARGS" ]
# Проверка числа аргументов.
then
 echo "Проядок использования: `basename $0` old-pattern new-pattern filename"
 exit $E_BADARGS
fi
 
old_pattern=$1
new_pattern=$2
 
if [ -f "$3" ]
then
 file_name=$3
else
 echo "Файл \"$3\" не найден."
 exit $E_BADARGS
fi
# Здесь, собственно, выполняется сама работа по поиску и замене.
sed -e "s/$old_pattern/$new_pattern/g" $file_name
# 's' — команда "substitute" (замены),
# а /pattern/ — задаёт шаблон искомого текста.
# "g" — флаг "global" (всеобщий), означает "выполнить подстановку для *каждого*
# обнаруженного $old_pattern во всех строках, а не только в первой строке.
 
exit 0 # При успешном завершении сценария — вернуть 0.

Пример: Сценарий-обертка вокруг сценария awk.

#!/bin/bash
# Суммирует числа в заданном столбце из заданного файла.
 
ARGS=2
E_WRONGARGS=65
 
if [ $# -ne "$ARGS" ] # Проверка числа аргументов.
then
 echo "Порядок использования: `basename $0` имя_файла номер_столбца"
 exit $E_WRONGARGS
fi
 
filename=$1
column_number=$2
 
# Здесь используется прием передачи переменных
# из командной оболочки в сценарий awk .
# Многострочный сценарий awk должен записываться в виде: awk ' ..... '
 
# Начало awk-сценария.
# -----------------------------
awk '
{ total += $'"${column_number}"'
}
END {
 print total
}
' "$filename"
# -----------------------------
# Конец awk-сценария.
 
# С точки зрения безопасности, передача shell-переменных
# во встроенный awk-скрипт, потенциально опасна,
# поэтому, Stephane Chazelas предлагает следующую альтернативу:
# ---------------------------------------
# awk -v column_number="$column_number" '
# { total += $column_number
# }
# END {
# print total
# }' "$filename"
# ---------------------------------------
exit 0

Для сценариев, которые должны строиться по принципу швейцарского армейского ножа — "всё водном", можно порекомендовать Perl. Perl совмещает в себе мощь и гибкость sed, awk и языка программирования C. Он поддерживает модульность и объектно-ориентированный стиль программирования. Короткие сценарии Perl могут легко встраиваться в сценарии командной оболочки, и даже полностью заменить из (хотя автор весьма скептически относится к последнему утверждению).

Пример: Сценарий на языке Perl, встроенный в Bash-скрипт.

#!/bin/bash
 
# Это команды shell, предшествующий сценарию на Perl.
echo "Эта строка выводится средствами Bash, перед выполнением встроенного Perl-скрипта, в \"$0\"."
echo
"=============================================================================================="
 
perl -e 'print "Эта строка выводится средствами Perl.\n";'
# Подобно sed, Perl тоже использует ключ "-e".
 
echo "====================================="
 
exit 0

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

Пример: Комбинирование сценария Bash и Perl в одном файле.

#!/bin/bash
#!/bin/bash
# bashandperl.sh
echo "Вас приветствует часть сценария, написанная на Bash."
 
# Далее могут следовать другие команды Bash.
 
exit 0
# Конец сценария на Bash.
 
# =======================================================
 
#!/usr/bin/perl
# Эта часть сценария должна вызываться с ключом -x.
 
print "Вас приветствует часть сценария, написанная на Perl.\n";
# Далее могут следовать другие команды Perl.
 
# Конец сценария на Perl.
 
bash$ bash bashandperl.sh 
Вас приветствует часть сценария, написанная на Bash.
 
bash$ perl -x bashandperl.sh
Вас приветствует часть сценария, написанная на Perl.

Операции сравнения: Альтернативные решения

Операции сравнения, выполняемые с помощью конструкции [[ ]], могут оказаться предпочтительнее,чем [ ]. Аналогично, при сравнении чисел, в более выгодном свете представляется конструкция (( )).

a=8
 
# Все, приведенные ниже, операции сравнения — эквивалентны.
test "$a" -lt 16 && echo "да, $a < 16" # "И-список"
/bin/test "$a" -lt 16 && echo "да, $a < 16"
[ "$a" -lt 16 ] && echo "да, $a < 16"
[[ $a -lt 16 ]] && echo "да, $a < 16" # Внутри [[ ]] и (( )) переменные
(( a < 16 )) && echo "да, $a < 16"    # не обязательно брать в кавычки.
 
city="New York"
# Опять же, все, приведенные ниже, операции — эквивалентны.
test "$city" \< Paris && echo "Да, Paris больше, чем $city" # В смысле ASCII-строк.
/bin/test "$city" \< Paris && echo "Да, Paris больше, чем $city"
[ "$city" \< Paris ] && echo "Да, Paris больше, чем $city"
[[ $city < Paris ]] && echo "Да, Paris больше, чем $city" # Кавычки вокруг $city не обязательны.
# Спасибо S.C.

Рекурсия

Может ли сценарий рекурсивно вызывать себя самого? Да, может!

Пример: Сценарий (бесполезный), который вызывает себя сам.

#!/bin/bash
# recurse.sh
 
# Может ли сценарий вызвать себя сам?
# Да, но есть ли в этом смысл?
 
RANGE=10
MAXVAL=9
i=$RANDOM
let "i %= $RANGE" # Генерация псевдослучайного числа в диапазоне 0 .. $MAXVAL.
 
if [ "$i" -lt "$MAXVAL" ]
then
 echo "i = $i"
 ./$0 # Сценарий запускает новый экземпляр себя самого.
fi # если число $i больше или равно $MAXVAL.
# Если конструкцию "if/then" заменить на цикл "while", то это вызовет определенные проблемы.
# Объясните — почему?.
 
exit 0

Пример: Сценарий (имеющий практическую ценность), который вызывает себя сам.

#!/bin/bash
#!/bin/bash
# pb.sh: телефонная книга
# Автор: Rick Boivie
# используется с его разрешения.
# Дополнен автором документа.
 
MINARGS=1 # Сценарию должен быть передан, по меньшей мере, один аргумент.
DATAFILE=./phonebook
PROGNAME=$0
E_NOARGS=70 # Ошибка, нет аргументов.
 
if [ $# -lt $MINARGS ]; then
 echo "Порядок использования: "$PROGNAME" data"
 exit $E_NOARGS
fi
 
if [ $# -eq $MINARGS ]; then
 grep $1 "$DATAFILE"
else
 ( shift; "$PROGNAME" $* ) | grep $1
 # Рекурсивный вызов.
fi
exit 0 # Сценарий завершает свою работу здесь.
       # Далее следует пример файла телефонной книги
       # в котором не используются символы комментария.
 
# ------------------------------------------------------------------------
# Пример файла телефонной книги
#John Doe 1555 Main St., Baltimore, MD 21228 (410) 222-3333
#Mary Moe 9899 Jones Blvd., Warren, NH 03787 (603) 898-3232
#Richard Roe 856 E. 7th St., New York, NY 10009 (212) 333-4567
#Sam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678
#Zoe Zenobia 4481 N. Baker St., San Franciso, SF 94338 (415) 501-1631
# ------------------------------------------------------------------------
 
$bash pb.sh Roe
Richard Roe 856 E. 7th St., New York, NY 10009 (212) 333-4567
Sam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678
 
$bash pb.sh Roe Sam
Sam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678
# Если сценарию передаются несколько аргументов,
# то выводятся только те строки, которые содержат их все.

Пример: Ещё один сценарий, который вызывает себя сам.

#!/bin/bash
# usrmnt.sh, автор Anthony Richardson
# Используется с его разрешения.
 
# Порядок использования: usrmnt.sh
# Описание: монтирует устройство, пользователь должен входить в состав группы
# MNTUSERS в файле /etc/sudoers.
 
# -----------------------------------------------------------------
# Этот сценарий рекурсивно вызывает себя самого,
# используя sudo. Пользователь, наделённый
# соответствующими правами может просто дать команду
# usermount /dev/fd0 /mnt/floppy
# вместо
# sudo usermount /dev/fd0 /mnt/floppy
# Подобную технику я использую во всех моих
# sudo-сценариях, поскольку она кажется мне достаточно удобной.
# -----------------------------------------------------------------
 
# Если переменная SUDO_COMMAND не определена, значит сценарий запущен не через
# sudo, поэтому повторно вызываем сценарий.
# Передавая user id и group id через переменные...
 
if [ -z "$SUDO_COMMAND" ]
then
 mntusr=$(id -u) grpusr=$(id -g) sudo $0 $*
 exit 0
fi
# В эту точку мы попадаем только если сценарий запущен через sudo
/bin/mount $* -o uid=$mntusr,gid=$grpusr
 
exit 0
# Дополнительные замечания от автора сценария:
# -------------------------------------------------
 
# 1) Linux допускает указание опции "users" в файле /etc/fstab,
# которая позволяет монтировать носители любому пользователю.
# Но на сервере я предпочитаю дать это право лишь отдельным
# пользователям. На мой взгляд sudo делает ситуацию более управляемой.
 
# 2) Я так же считаю, что утилита sudo более удобна, чем
# выполнение той же задачи посредством групп.
 
# 3) Эта методика выдает права root на доступ к команде
# mount, что требует особого внимания при выделении пользователей
# наделенных таким правом. Используя ту же самую технику,
# вы можете более точно разграничить права монтирования
# устройств, написав сценарии mntfloppy, mntcdrom и mntsamba.

Слишком глубокая рекурсия может привести к исчерпанию пространства, выделенного под стек, и "вываливанию" сценария по "segfault".

"Цветные" сценарии

Для установки атрибутов отображения информации на экране, таких как: жирный текст, цвет символов, цвет фона и т.п., с давних пор используются ANSI escape-последовательности. Эти последовательности широко используются в пакетных файлах DOS, эти же последовательности используются и в сценариях Bash.

Пример: "Цветная" адресная книга.

#!/bin/bash
# ex30a.sh: Версия сценария ex30.sh, с добавлением цвета .
# Грубый пример базы данных
 
clear # Очистка экрана
 
echo -n "             "
echo -e '\E[37;44m'"\033[1mСписок\033[0m"
                    # Белый текст на синем фоне
echo; echo
echo -e "\033[1mВыберите интересующую Вас персону:\033[0m"
                    # Жирный шрифт
tput sgr0
echo "(Введите только первую букву имени.)"
echo
echo -en '\E[47;34m'"\033[1mE\033[0m" # Синий
tput sgr0 # сброс цвета
echo "vans, Roland" # "[E]vans, Roland"
echo -en '\E[47;35m'"\033[1mJ\033[0m" # Пурпурный
tput sgr0
echo "ones, Mildred"
echo -en '\E[47;32m'"\033[1mS\033[0m" # Зелёный
tput sgr0
echo "mith, Julie"
echo -en '\E[47;31m'"\033[1mZ\033[0m" # Красный
tput sgr0
echo "ane, Morris"
 
echo
read personcase "$person" in
# Обратите внимание: переменная взята в кавычки.
 "E" | "e" )
 # Пользователь может ввести как заглавную, так и строчную букву.
 echo
 echo "Roland Evans"
 echo "4321 Floppy Dr."
 echo "Hardscrabble, CO 80753"
 echo "(303) 734-9874"
 echo "(303) 734-9892 fax"
 echo "revans@zzy.net"
 echo "Старый друг и партнер по бизнесу"
 ;;
 
 "J" | "j" )
 echo
 echo "Mildred Jones"
 echo "249 E. 7th St., Apt. 19"
 echo "New York, NY 10009"
 echo "(212) 533-2814"
 echo "(212) 533-9972 fax"
 echo "milliej@loisaida.com"
 echo "Подружка"
 echo "День рождения: 11 февраля"
 ;;
 
# Информация о Smith и Zane будет добавлена позднее.
 * )
 # Выбор по-умолчанию.
 # "Пустой" ввод тоже обрабатывается здесь.
 echo
 echo "Нет данных."
 ;;
 
esac
tput sgr0
# Сброс цвета
echo
 
exit 0

Самая простая и, на мой взгляд, самая полезная escape-последовательность — это "жирный текст", \033[1m ... \033[0m. Здесь, комбинация \033 представляет escape-символ, комбинация [1 — включает вывод жирным текстом, а [0 — выключает. Символ m — завершает каждую из escape-последовательностей.

bash$ echo -e "\033[1mЭто жирный текст.\033[0m"

Простая escape-последовательность, которая управляет атрибутом подчеркивания (в rxvt и aterm).

bash$ echo -e "\033[4mЭто подчеркнутый текст.\033[0m"

Ключ -e, в команде echo, разрешает интерпретацию escape-последовательностей.
Другие escape-последовательности, изменяющие атрибуты цвета:

bash$ echo -e '\E[34;47mЭтот текст выводится синим цветом.'; tput sgr0
 
bash$ echo -e '\E[33;44m'"жёлтый текст на синем фоне"; tput sgr0

Команда tput sgr0 возвращает настройки терминала в первоначальное состояние.<br /

Вывод цветного текста осуществляется по следующему шаблону:
echo -e '\E[COLOR1;COLOR2mКакой либо текст.'

Где "\E[" — начало escape-последовательности. Числа "COLOR1" и "COLOR2", разделённые точкой с запятой, задают цвет символов и цвет фона, в соответствии с таблицей цветов, приведенной ниже. (Порядок указания цвета текста и фона не имеет значения, поскольку диапазоны числовых значений цвета для текста и фона не пересекаются). Символ "m" — должен завершать escape-последовательность.
Обратите внимание: одиночные кавычки окружают всё, что следует за echo -e.

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

Таблица. Числовые значения цвета в escape-последовательностях

Цвет Текст Фон
Чёрный 30 40
Красный 31 41
Зелёный 32 42
Жёлтый 33 43
Синий 34 44
Пурпурный 35 45
Зеленовато-голубой 36 46
Белый 37 47

Пример: Вывод цветного текста.

#!/bin/bash
# color-echo.sh: Вывод цветных сообщений.
 
black='\E[30;47m'
red='\E[31;47m'
green='\E[32;47m'
yellow='\E[33;47m'
blue='\E[34;47m'
magenta='\E[35;47m'
cyan='\E[36;47m'
white='\E[37;47m'
 
cecho ()                          # Color-echo.
                                  # Аргумент $1 = текст сообщения
                                  # Аргумент $2 = цвет
{
 local default_msg="Нет сообщений."
                                  # Не обязательно должна быть локальной.
 
 message=${1:-$default_msg}       # Текст сообщения по-умолчанию.
 color=${2:-$black}               # Цвет по-умолчанию чёрный.
 
 echo -e "$color"
 echo "$message"
 tput sgr0                        # Восстановление первоначальных настроек терминала.
 return
}
 
# Попробуем что-нибудь вывести.
# ----------------------------------------------------
cecho "Синий текст..." $blue
cecho "Пурпурный текст." $magenta
cecho "Позеленевший от зависти." $green
cecho "Похоже на красный?" $red
cecho "Циан, более известный как цвет морской волны." $cyan
cecho "Цвет не задан (по-умолчанию чёрный)."
       # Аргумент $color отсутствует.
cecho "\"Пустой\" цвет (по-умолчанию чёрный)." ""
       # Передан "пустой" аргумент цвета.
cecho
       # Ни сообщение ни цвет не переданы.
cecho "" ""
       # Функции переданы "пустые" аргументы $message и $color.
# ----------------------------------------------------
 
echo
exit 0
# Упражнения:
# ---------
# 1) Добавьте в функцию 'cecho ()' возможность вывода "жирного текста".
# 2) Добавьте возможность управления цветом фона.

Однако, как обычно, в бочке меда есть ложка дегтя. Escape-последовательности ANSI совершенно не переносимы. Вывод в одном эмуляторе терминала (или в консоли) может разительно отличаться от вывода в другом эмуляторе. "Расцвеченные" сценарии, дающие изумительно красивый вывод текста на одном терминале, могут давать совершенно нечитаемый текст на другом. Это ставит под сомнение практическую ценность "расцвечивания" вывода в сценариях, низводя её до уровня никчёмной "игрушки".
Moshe Jacobson разработал утилиту color (http://runslinux.net/projects/color), которая значительно упрощает работу с ANSI escape-последовательностями, заменяя, только что обсуждавшиеся, неуклюжие конструкции, логичным и понятным синтаксисом.

Оптимизация

По большей части, сценарии на языке командной оболочки, используются для быстрого решения несложных задач. Поэтому оптимизация сценариев, по скорости исполнения, не является насущной проблемой. Тем не менее, представьте себе ситуацию, когда сценарий, выполняющий довольно важную работу, в принципе справляется со своей задачей, но делает это очень медленно. Написание же аналогичной программы на языке компилирующего типа — неприемлемо. Самое простое решение — переписать самые медленные участки кода сценария. Возможно ли применить принципы оптимизации к сценарию на практике?
Для начала проверьте все циклы в сценарии. Основная масса времени уходит на работу в циклах. Если это возможно, вынесите все ресурсоёмкие операции за пределы циклов. Старайтесь использовать встроенные команды. Они исполняются значительно быстрее и, как правило, не запускают подоболочку при вызове. Избегайте использования избыточных команд, особенно это относится к конвейерам.

cat "$file" | grep "$word"
 
grep "$word" "$file"
 
# Эти команды дают один и тот же результат,
# но вторая работает быстрее, поскольку запускает на один подпроцесс меньше.

Не следует злоупотреблять командой cat.
Для профилирования сценариев, можно воспользоваться командами time и times. Не следует пренебрегать возможностью переписать особенно критичные участки кода на языке C или даже на ассемблере .
Попробуйте минимизировать количество операций с файлами. Bash не "страдает" излишней эффективностью при работе с файлами, попробуйте применить специализированные средства для работы с файлами в сценариях, такие как awk или Perl.
Записывайте сценарии в структурированной форме, это облегчит их последующую реорганизацию и оптимизацию. Помните, что значительная часть методов оптимизации кода, существующих в языках высокого уровня, вполне применима и к сценариям, однако есть и такие, которые не могут применяться. Основной критерий здесь — это здравый смысл.

Разные советы

  • Для ведения учета использования сценария пользователями, добавьте следующие строки в сценарий. Они запишут в файл отчёта название сценария и время запуска.
# Добавление (>>) учетной записи, об использовании сценария, в файл отчета.
 
date>> $SAVE_FILE       # Дата и время.
echo $0>> $SAVE_FILE    # Название сценария.
echo>> $SAVE_FILE       # Пустая строка — как разделитель записей.
 
# Не забудьте определить переменную окружения SAVE_FILE в ~/.bashrc
# (что нибудь, типа: ~/.scripts-run)

  • Оператор >> производит добавление строки в конец файла. А как быть, если надо добавить строку в начало существующего файла?
file=data.txt
title="***Это титульная строка в текстовом файле***"
 
echo $title | cat - $file >$file.new
# "cat -" объединяет stdout с содержимым $file.
# В результате получится
# новый файл $file.new, в начало которого добавлена строка $title.

Само собой разумеется, то же самое можно сделать с помощью sed.

  • Сценарий командной оболочки может использоваться как команда внутри другого сценария командной оболочки, Tcl, или wish сценария или, даже в Makefile . Он может быть вызван как внешняя команда из программы на языке C, с помощью функции system(), т.е. system("script_name");.
  • Собирайте свои библиотеки часто используемых функций и определений . Эти "библиотеки" могут быть "подключены" к сценариям, с помощью команды точка (.) или source.
# Сценарий-библиотека
# ------ -------
 
# Обратите внимание:
# Здесь нет sha-bang ("#!").
# И нет "живого кода".
 
# Определения переменных
ROOT_UID=0               # UID root-а, 0.
E_NOTROOT=101            # Ошибка — "обычный пользователь".
MAXRETVAL=255            # Максимальное значение, которое могут возвращать функции.
SUCCESS=0
FAILURE=-1
# Функции
Usage ()                 # Сообщение "Порядок использования:".
{
 if [ -z "$1" ]          # Нет аргументов.
 then
  msg=filename
 else
  msg=$@
 fi
 echo "Порядок использования: `basename $0` "$msg""
}
 
Check_if_root ()          # Проверка прав пользователя.
{                         # из примера "ex39.sh".
 if [ "$UID" -ne "$ROOT_UID" ]
 then
  echo "Этот сценарий должен запускаться с привилегиями root."
  exit $E_NOTROOT
 fi
}
 
CreateTempfileName ()      # Создание "уникального" имени для временного файла.
{                          # Из примера "ex51.sh".
 prefix=temp
 suffix=`eval date +%s`
 Tempfilename=$prefix.$suffix
}
 
isalpha2 ()                 # Проверка, состоит ли строка только из алфавитных символов.
{                           # Из примера "isalpha.sh".
 [ $# -eq 1 ] || return $FAILURE
 
 case $1 in
 *[!a-zA-Z]*|"") return $FAILURE;;
 *) return $SUCCESS;;
 esac                       # Спасибо S.C.
}
 
abs ()                      # Абсолютное значение.
{                           # Внимание: Максимально возможное возвращаемое значение
                            # не может превышать 255.
 E_ARGERR=-999999
 
 if [ -z "$1" ]             # Проверка наличия входного аргумента.
 then
  return $E_ARGERR          # Код ошибки, обычно возвращаемый в таких случаях.
 fi
 
 if [ "$1" -ge 0 ]            # Если не отрицательное,
 then                         #
  absval=$1                   # оставить как есть.
 else                         # Иначе,
  let "absval = (( 0 - $1 ))" # изменить знак.
 fi
 
 return $absval
}
 
tolower ()                    # Преобразование строк символов в нижний регистр
{
 if [ -z "$1" ]               # Если нет входного аргумента,
 then                         # выдать сообщение об ошибке
 echo "(null)"
 return                       # и выйти из функции.
 fi
 echo "$@" | tr A-Z a-z
 # Преобразовать все входные аргументы ($@).
 return
# Для записи результата работы функции в переменную, используйте операцию подстановки команды.
# Например:
# oldvar="A seT of miXed-caSe LEtTerS"
# newvar=`tolower "$oldvar"`
# echo "$newvar" # a set of mixed-case letters
#
# Упражнение: Добавьте в эту библиотеку функцию перевода символов в верхний регистр.
# toupper() [это довольно просто].
}

  • Для повышения ясности комментариев, выделяйте их особым образом.
## Внимание!
rm -rf *.zzy           ## Комбинация ключей "-rf", в команде "rm", чрезвычайно опасна,
                       ## особенно при удалении по шаблону.
# Продолжение комментария на новой строке.
# Это первая строка комментария
# это вторая строка комментария,
# это последняя строка комментария.
 
#* Обратите внимание.
#o Элемент списка.
#> Альтернативный вариант.
while [ "$var1" != "end" ] #> while test "$var1" != "end"

  • Для создания блочных комментариев, можно использовать конструкцию if-test.
#!/bin/bash
COMMENT_BLOCK=
# Если попробовать инициализировать эту переменную чем-нибудь,
# то вы получите неожиданный результат.
 
if [ $COMMENT_BLOCK ]; then
Блок комментария --
=================================
Это строка комментария.
Это другая строка комментария.
Это ещё одна строка комментария.
=================================
echo "Эта строка не выводится."
Этот блок комментария не вызывает сообщения об ошибке! Круто!
fi
 
echo "Эта строка будет выведена на stdout."
 
exit 0

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

  • С помощью служебной переменной $?, можно проверить — является ли входной аргумент целым числом.
#!/bin/bash
SUCCESS=0
E_BADINPUT=65
test "$1" -ne 0 -o "$1" -eq 0 2>/dev/null
# Проверка: "равно нулю или не равно нулю".
# 2>/dev/null подавление вывода сообщений об ошибках.
 
if [ $? -ne "$SUCCESS" ]
then
 echo "Порядок использования: `basename $0` целое_число"
 exit $E_BADINPUT
fi
let "sum = $1 + 25"       # Будет выдавать ошибку, если $1 не является целым числом.
echo "Sum = $sum"
# Любая переменная может быть проверена таким образом, а не только входные аргументы.
 
exit 0

  • Диапазон, возвращаемых функциями значений, 0-255 — серьёзное ограничение. Иногда может оказаться весьма проблематичным использование глобальных переменных, для передачи результата из функции. В таких случаях можно порекомендовать передачу результатов работы функции через запись в stdout. Фактически этот приём является разновидностью подстановки команд.

Пример: Необычный способ передачи возвращаемого значения.

#!/bin/bash
# multiplication.sh
multiply ()                                  # Функции выполняет перемножение всех переданных аргументов.
{
 local product=1
 until [ -z "$1" ]                           # Пока не дошли до последнего аргумента...
 do
  let "product *= $1"
  shift
 done
 echo $product                               # Значение не будет выведено на экран,
}                                            # поскольку оно будет записано в переменную.
 
mult1=15383; mult2=25211
val1=`multiply $mult1 $mult2`
echo "$mult1 X $mult2 = $val1"
                                              # 387820813
mult1=25; mult2=5; mult3=20
val2=`multiply $mult1 $mult2 $mult3`
echo "$mult1 X $mult2 X $mult3 = $val2"
                                 # 2500
 
mult1=188; mult2=37; mult3=25; mult4=47
val3=`multiply $mult1 $mult2 $mult3 $mult4`
echo "$mult1 X $mult2 X $mult3 X mult4 = $val3"
                                # 8173300
exit 0

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

capitalize_ichar ()       # Первый символ всех строковых аргументов
{                         # переводится в верхний регистр.
 string0="$@"             # Принять все аргументы.
 
 firstchar=${string0:0:1} # Первый символ.
 string1=${string0:1}     # Остаток строки.
 FirstChar=`echo "$firstchar" | tr a-z A-Z`
                              # Преобразовать в верхний регистр.
 echo "$FirstChar$string1" # Выдать на stdout.
}
newstring=`capitalize_ichar "each sentence should start with a capital letter."`
echo "$newstring" # Each sentence should start with a capital letter.

Используя этот приём, функция может "возвращать" даже несколько значений.

Пример: Необычный способ получения нескольких возвращаемых значений.

#!/bin/bash
# sum-product.sh
# Функция может "возвращать" несколько значений.
 
sum_and_product () # Вычисляет сумму и произведение аргументов.
{
 echo $(( $1 + $2 )) $(( $1 * $2 ))
# Вывод на stdout двух значений, разделенных пробелом.
}
 
echo
echo "Первое число: "
read first
 
echo
echo "Второе число: "
read second
echo
 
retval=`sum_and_product $first $second` # Получить результат.
sum=`echo "$retval" | awk '{print $1}'` # Первое значение (поле).
product=`echo "$retval" | awk '{print $2}'` # Второе значение (поле).
 
echo "$first + $second = $sum"
echo "$first * $second = $product"
echo
 
exit 0

  • Следующая хитрость — передача массива в функцию, и "возврат" массива из функции.

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

Пример: Передача массива в функцию и возврат массива из функции.

#!/bin/bash
# array-function.sh: Передача массива в функцию и...
# "возврат" массива из функции
Pass_Array ()
{
 local passed_array # Локальная переменная.
 passed_array=( `echo "$1"` )
 echo "${passed_array[@]}"
 # Список всех элементов в новом массиве,
 # объявленном и инициализированном в функции.
}
 
original_array=( element1 element2 element3 element4 element5 )
 
echo
echo "original_array = ${original_array[@]}"
#                       Список всех элементов исходного массива.
 
# Так можно отдать массив в функцию.
# **********************************
argument=`echo ${original_array[@]}`
# **********************************
# Поместив все элементы массива в переменную, разделяя их пробелами.
#
# Обратите внимание: метод прямой передачи массива в функцию не сработает.
 
# Так можно получить массив из функции.
# *****************************************
returned_array=( `Pass_Array "$argument"` )
# *****************************************
# Записать результат в переменную-массив.
 
echo "returned_array = ${returned_array[@]}"
 
echo "============================================================="
 
# А теперь попробуйте получить доступ к локальному массиву
# за пределами функции.
Pass_Array "$argument"
 
# Функция выведет массив, но...
# доступ к локальному массиву, за пределами функции, окажется невозможен.
echo "Результирующий массив (внутри функции) = ${passed_array[@]}"
# "ПУСТОЕ" ЗНАЧЕНИЕ, поскольку это локальная переменная.
 
echo
exit 0

  • Использование конструкций с двойными круглыми скобками позволяет применять C-подобный синтаксис операций присвоения и инкремента переменных, а также оформления циклов for и while .
  • Иногда очень удобно "пропускать" данные через один и тот же фильтр, но с разными параметрами, используя конвейерную обработку. Особенно это относится к tr и grep.
# Из примера "wstrings.sh".
wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \
tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '`

Пример: Игры с анаграммами.

#!/bin/bash
# agram.sh: Игры с анаграммами.
 
# Поиск анаграмм...
LETTERSET=etaoinshrdlu
anagram "$LETTERSET" |          # Найти все анаграммы в наборе символов...
grep '.......' |                # состоящие, как минимум из 7 символов,
grep '^is' |                    # начинающиеся с 'is'
grep -v 's$' |                  # исключая множественное число
grep -v 'ed$'                   # и глаголы в прошедшем времени
 
# Здесь используется утилита "anagram"
# которая входит в состав пакета "yawl" , разработанного автором.
# http://ibiblio.org/pub/Linux/libs/yawl-0.2.tar.gz
exit 0 
# Конец.
 
bash$ sh agram.sh
islander
isolate
isolead
isotheral

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

  • Попытка вызова утилиты из сценария на машине, где эта утилита отсутствует, потенциально опасна. Для обхода подобных проблем можно воспользоваться утилитой whatis.
CMD=command1 # Основной вариант.
PlanB=command2 # Запасной вариант.
 
command_test=$(whatis "$CMD" | grep 'nothing appropriate')
# Если 'command1' не найдена в системе, то 'whatis' вернёт
# "command1: nothing appropriate."
#==> От переводчика: Будьте внимательны! Если у вас локализованная версия whatis
#==> то вывод от неё может отличаться от используемого здесь ('nothing appropriate')
 
if [[ -z "$command_test" ]] # Проверка наличия утилиты в системе.
then
 $CMD option1 option2       # Запуск команды с параметрами.
else                        # Иначе,
$PlanB                      # запустить command2 (запасной вариант).
fi

  • Команда run-parts удобна для запуска нескольких сценариев, особенно в комбинации с cron или at .
  • Иногда было бы неплохо снабдить сценарий графическим интерфейсом X-Window . Для этого можно порекомендовать пакеты Xscript, Xmenu и widtools. Правда, первые два, кажется больше не поддерживаются разработчиками. Зато widtools можно получить здесь.

Пакет widtools (widget tools) требует наличия библиотеки XForms. Кроме того, необходимо слегка подправить Makefile, чтобы этот пакет можно было собрать на типичной Linux-системе. Но хуже всего то, что три из шести виджетов не работают :-(( (segfault).
Утилита dialog — ещё один способ создания диалоговых форм из сценариев командной оболочки. Эта утилита предназначена для работы в текстовой консоли, но имеются её "наследники" — gdialog, Xdialog и kdialog, которые используют графические элементы X-Windows для построения диалоговых форм.

Пример: Сценарий с графическим интерфейсом.

#!/bin/bash
# dialog.sh: Использование виджетов 'gdialog'.
# В вашей системе должна быть установлена утилита 'gdialog'.
 
# Идея создания этого сценария появилась после прочтения статьи
# "Scripting for X Productivity," by Marco Fioretti,
# LINUX JOURNAL, Issue 113, September 2003, pp. 86-9.
# Спасибо всем сотрудникам LJ.
 
# Ошибка ввода в диалоговом окне.
E_INPUT=65
# Размеры окна.
HEIGHT=50
WIDTH=60
 
# Имя выходного файла (конструируется добавлением суффикса к имени файла-сценария).
OUTFILE=$0.output
 
# Вывести содержимое файла-сценария в отдельном окне.
gdialog --title "Displaying: $0" --textbox $0 $HEIGHT $WIDTH
 
# Попробуем записать значение, введенное в окне.
echo -n "VARIABLE=\"" > $OUTFILE # Кавычка на случай, если пользователь введет несколько слов, разделенных пробелами.
gdialog --title "User Input" --inputbox "Введите значение переменной:" \
$HEIGHT $WIDTH >> $OUTFILE
 
if [ "$?" -eq 0 ]
# Хороший тон — проверка кода завершения.
then
 echo "Диалог с пользователем завершился без ошибок."
else
 echo "Обнаружены ошибки во время диалога с пользователем."
      # Или была нажата кнопка "Отменить" ("Cancel") вместо "OK".
 rm $OUTFILE
 exit $E_INPUT
fi
echo -n "\"" >> $OUTFILE # Завершающая кавычка.
# Теперь прочитаем значение переменной из файла и выведем его.
. $OUTFILE # 'Выходной' файл.
echo "Было введено значение переменной: "$VARIABLE""
rm $OUTFILE # Удалить временный файл.
exit 0

Кроме того, для построения приложений с графическим интерфейсом, можно попробовать Tk, или wish (надстройка над Tcl), PerlTk (Perl с поддержкой Tk), tksh (ksh с поддержкой Tk), XForms4Perl (Perl с поддержкой XForms), Gtk-Perl (Perl с поддержкой Gtk) или PyQt (Python с поддержкой Qt).

Проблемы безопасности

Уместным будет лишний раз предупредить о соблюдении мер предосторожности при работе с незнакомыми сценариями. Сценарий может содержать червя, трояна или даже вирус. Если вы получили сценарий не из источника, которому доверяете, то никогда не запускайте его с привилегиями root и не позволяйте вставлять его в список сценариев начальной инициализации системы в /etc/rc.d, пока не убедитесь в том, что он безвреден для системы. Исследователи из Bell Labs и других организаций, включая M. Douglas McIlroy, Tom Duff, и Fred Cohen исследовали вопрос о возможности создания вирусов на языке сценариев командной оболочки, и пришли к выводу, что это делается очень легко и доступно даже для новичков. Это ещё одна из причин, по которым следует изучать язык командной оболочки. Способность читать и понимать сценарии поможет вам предотвратить возможность взлома и/или разрушения вашей системы.

Проблемы переносимости

Эта книга делает упор на создании сценариев для командной оболочки Bash, для операционной системы GNU/Linux. Тем не менее, многие рекомендации, приводимые здесь, могут быть вполне применимы и для других командных оболочек, таких как sh и ksh. Многие версии командных оболочек стремятся следовать стандарту POSIX 1003.2. Вызывая Bash с ключом --posix, или вставляя set -o posix в начало сценария, вы можете заставить Bash очень близко следовать этому стандарту. Но, даже без этого ключа, большинство сценариев, написанных для Bash, будут работать под управлением ksh, и наоборот, т.к. Chet Ramey перенес многие особенности, присущие ksh, в последние версии Bash. В коммерческих версиях Unix, сценарии, использующие GNU-версии стандартных утилит и команд, могут оказаться неработоспособными. Однако, с течением времени, таких проблем остается всё меньше и меньше, поскольку утилиты GNU, в большинстве своем, заместили свои проприетарные аналоги в Unix. После того, как Caldera дала разрешение на публикацию исходного кода некоторых версий оригинальных утилит Unix, этот процесс значительно ускорился. Bash имеет некоторые особенности, недоступные в традиционном Bourne shell. Среди них:

  • Некоторые дополнительные ключи вызова
  • Подстановка команд, с использованием нотации $( )
  • Некоторые операции над строками
  • Подстановка процессов
  • Встроенные команды Bash

Более подробный список характерных особенностей Bash, вы найдете в Bash F.A.Q..

Сценарии командной оболочки под Windows

Даже те пользователи, которые работают в другой, не Unix-подобной операционной системе, смогут запускать сценарии командной оболочки, а потому — найти для себя много полезного в этой книге. Пакеты Cygwin от Cygnus, и MKS utilities от Mortice Kern Associates, позволяют дополнить Windows возможностями командной оболочки.

Bash, версия 2

Текущая версия Bash, та, которая скорее всего установлена в вашей системе, фактически — 2.XX.Y(На момент написания/перевода книги).

bash$ echo $BASH_VERSION
2.05.b.0(1)-release

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

Пример: Расширение строк.

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

Пример: Косвенные ссылки на переменные — новый метод.

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

Пример: Простая база данных, с применением косвенных ссылок.

#!/bin/bash
# resistor-inventory.sh
 
# Простая база данных, с применением косвенных ссылок.
# ============================================================== #
# Данные
 
B1723_value=470                                # сопротивление (Ом)
B1723_powerdissip=.25                          # рассеиваемая мощность (Вт)
B1723_colorcode="желтый-фиолетовый-коричневый" # цветовая маркировка
B1723_loc=173                                  # где
B1723_inventory=78                             # количество (шт)
 
B1724_value=1000
B1724_power
dissip=.25
B1724_colorcode="коричневый-черный-красный"
B1724_loc=24
NB1724_inventory=243
 
B1725_value=10000
B1725_powerdissip=.25
B1725_colorcode="коричневый-черный-оранжевый"
B1724_loc=24
NB1724_inventory=243
 
B1725_value=10000
B1725_powerdissip=.25
B1725_colorcode="коричневый-черный-оранжевый"
B1725_loc=24
NB1725_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

Пример: Массивы и другие хитрости для раздачи колоды карт в четыре руки.

#!/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 Deckdeclare -a Suitsdeclare -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:
# Упростите логику сценария.

Замечания и дополнения

От автора

Как я пришёл к мысли о написании этой книги? Это необычная история. Случилось это лет несколько тому назад. Мне потребовалось изучить язык командной оболочки — а что может быть лучше, как нечтение хорошей книги!? Я надеялся купить учебник и справочник, которые охватывали бы в полной мере данную тематику. Я искал книгу, которая возьмёт трудные понятия, вывернет их наизнанку и подробно разжует на хорошо откомментированных примерах. В общем, я искал очень хорошую книгу. К сожалению, в природе таковой не существовало, поэтому я счёл необходимым написать её.
Это напоминает мне сказку о сумасшедшем профессоре. Помешанный, до безумия, при виде книги, любой книги — в библиотеке, в книжном магазине — не важно где, им овладевала уверенность в том, что и он мог бы написать эту книгу, причём мог бы сделать это гораздо лучше. Он стремительно мчался домой и садился за создание своей собственной книги с тем же названием. Когда он умер, в его доме нашли несколько тысяч, написанных им книг, этого количества хватило бы, чтобы посрамить самого Айзека Азимова. Книги, может быть и не были так хороши — кто знает, но разве это имеет какое-то значение? Вот — человек, жил своими грёзами, пусть одержимый и движимый ими, но я не могу удержаться от восхищения старым чудаком...

Об авторе

Автор не стремится ни к званиям, ни к наградам, им движет неодолимое желание писать. Эта книга — своего рода отдых от основной работы, HOW-2 Meet Women: The Shy Man's Guide to Relationships (Руководство Застенчивого Мужчины о том Как Познакомиться С Женщиной) . Он также написал Software-Building HOWTO. В последнее время он пробует себя в беллетристике.
Пользуется Linux с 1995 года (Slackware 2.2, kernel 1.2.1). Выпустил несколько программ, среди которых cruft — утилита шифрования, заменявшая стандартную Unix-овую crypt, mcalc — финансовый калькулятор, для выполнения расчетов по займам, judge и yawl — пакет игр со словами. Программировать начинал с языка FORTRAN IV на CDC 3800, но не испытывает ностальгии по тем дням. Живёт в глухой, заброшенной деревушке со своей женой и собакой.

Куда обращаться за помощью

Автор(thegrendel@theriver.com) этой книги обычно не оставляет без ответа задаваемые ему вопросы (если он не слишком занят и пребывает в добром расположении духа). Однако, если вы испытываете проблемы со специфическим сценарием, то вам лучше обратиться на [comp.os.unix.shell] Usenet newsgroup.

Инструменты, использовавшиеся при создании книги

Аппаратура

IBM Thinkpad, model 760XL laptop (P166, 104 Mb RAM) под управлением Red Hat 7.1/7.3. Несомненно,это довольно медлительный агрегат, но он имеет отличную клавиатуру, и это много лучше, чем пара карандашей и письменный стол.

Программное обеспечение

  • Мощный текстовый редактор vim (автор: Bram Moolenaar).
  • OpenJade — инструмент, выполняющий, на основе DSSSL, верификацию и преобразование SGML-документов в другие форматы.
  • Таблицы стилей DSSSL от Norman Walsh.
  • DocBook, The Definitive Guide (Norman Walsh, Leonard Muellner O'Reilly, ISBN 1-56592-580-7). Полное руководство по созданию документов в формате Docbook SGML.

Благодарности

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

  • Philippe Martin — перевёл этот документ в формат DocBook/SGML. Работает в маленькой французской компании, в качестве разработчика программного обеспечения. В свободное от работы время — любит работать над документацией или программным обеспечением для GNU/Linux, читать книги, слушать музыку и веселиться с друзьями. Вы можете столкнуться с ним, где-нибудь во Франции, в провинции Басков, или написать ему письмо на feloy@free.fr. Philippe Martin также отметил, что возможно использование позиционных параметров за $9, при использовании {фигурных скобок}.
  • Stephane Chazelas — выполнил титаническую работу по корректировке, дополнению и написанию примеров сценариев. Фактически, он взвалил на свои плечи обязанности редактора этого документа. Огромное спасибо!
  • Paulo Marcel Coelho Aragao предложил большое количество дополнений и изменений, как важных, таки не очень. А также дал ряд дельных советов.

Особенно я хотел бы поблагодарить Patrick Callahan, Mike Novak и Pal Domokos за исправление ошибок и неточностей, за разъяснения и дополнения. Их живое обсуждение проблем, связанных с созданием сценариев на языке командной оболочки вдохновило меня на попытку сделать этот документ более удобочитаемым.
Я благодарен Jim Van Zandt за выявленные им ошибки и упущения, в версии 0.2 этого документа, и за поучительный пример сценария.
Большое спасибо Jordi Sanfeliu за то, что он дал возможность использовать его прекрасный сценарий в этой книге.
Выражаю свою благодарность Michel Charpentier за разрешение использовать его dc сценарий разложения на простые множители.
Спасибо Noah Friedman, предоставившему право использовать его сценарий. Emmanuel Rouat предложил несколько изменений и дополнений в разделах, посвящённых подстановке команд и псевдонимам. Он так же предоставил замечательный пример файла .bashrc (Приложение I).
Heiner Steven любезно разрешил опубликовать его сценарий. Он сделал множество исправлений и внёс большое количество предложений. Особое спасибо!
Rick Boivie предоставил отличный сценарий, демонстрирующий рекурсию, pb.sh и внёс предложения по повышению производительности сценария monthlypmt.sh. Florian Wisser оказывал содействие при написании разделов, посвящённых строкам. Oleg Philon передал свои предложения относительно команд cut и pidof. Michael Zick расширил пример с пустыми массивами, введя туда демонстрацию необычных свойств массивов. Он также предоставил ряд других примеров. Marc-Jano Knopp выполнил исправления в разделе, посвящённом пакетным файлам DOS. Hyun Jin Cha, в процессе работы над корейским переводом, обнаружил несколько опечаток в документе. Спасибо ему за это! Andreas Abraham передал большое число типографских ошибок и внёс ряд исправлений. Особое спасибо!
Кроме того, я хотел бы выразить свою признательность за примеры сценариев, исправление моих ошибок и полезные советы Gabor Kiss, Leopold Toetsch, Peter Tillier, Marcus Berglof, Tony Richardson,Nick Drage, Rich Bartell, Jess Thrysoee, Adam Lazur, Bram Moolenaar, Baris Cicek, Greg Keraunen, KeithMatthews, Sandro Magi, Albert Reiner, Dim Segebart, Rory Winston, Lee Bigelow, Wayne Pollock, "jipe,"Emilio Conti, Dennis Leeuw, Dan Jacobson, Aurelio Marinho Jargas, Edward Scholtz, Jean Helou, ChrisMartin, Lee Maschmeyer, Bruno Haible, Sebastien Godard, Bjіn Eriksson, "nyal," John MacDonald, JoshuaTschida, Troy Engel, Manfred Schwarb, Amit Singh, Bill Gradwohl, David Lombard, Jason Parker, Bruce W.Clare, William Park, and David Lawyer(автор 4-х HOWTO).
Мои благодарности Chet Ramey(chet@po.cwru.edu) и Brian Fox за создание Bash — этого элегантного и мощного инструмента!
Особое спасибо добровольцам из Linux Documentation Project. Проект LDP сделал возможным публикацию этой книги в своём архиве.
Особую признательность хочу выразить IBM, Novell, Red Hat, Free Software Foundation и всем замечательным людям, которые бьются за то, чтобы программное обеспечение Open Source оставалось свободным и открытым.
Больше всего я хотел бы выразить свою благодарность моей супруге, Anita, за ее эмоциональную поддержку.

Библиография

  • Jeffrey Friedl, Mastering Regular Expressions, O'Reilly and Associates, 2002, 0-596-00289-0.
Прекрасное руководство по Регулярным Выражениям.
  • 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. Это обычный учебник. Здесь вы найдете превосходное введение в "регулярныевыражения". Обязательно прочитайте эту книгу.
  • Chet Ramey and Brian Fox, The GNU Bash Reference Manual, Network Theory Ltd, 2003, 0-9541617-7-7.
Это руководство является официальной документацией по GNU Bash. Авторы руководства, ChetRamey и Brian Fox, занимаются разработкой GNU Bash с самого начала. Издатель передаёт $1 в Фонд Свободного Программного Обеспечения (Free Software Foundation) с каждой проданной копии руководства.
  • 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 andAssociates, 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 PowerTools, 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..
  • Mike G Bash-Programming-Intro HOWTO.
  • Richard Unix Scripting Universe.
  • 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.
  • Fioretti, Marco, "Scripting for X Productivity," LINUX JOURNAL, Выпуск 113, Сентябрь, 2003, стр.86-9.
  • Aurelio Marinho Jargas написал Regular expression wizard. Он так же написал поучительную книгу, посвященную регулярным выражениям, на португальском(!) языке..
  • Ben Tomkins создал Bash Navigator — средство навигации по каталогам.
  • William Park работает над проектом по переносу некоторых возможностей из Awk и Python в 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.

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

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

Пример: 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: Очень простая утилита для переименования файлов.

#!/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-Sessionuser 
$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 
 
CDCDROM=/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 "Вставьте чистую болванку CD-R."
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=0                 # Выводить по 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 $yearstrip_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" ]                   # Проверить аргумент командной строки — файл с "поколением 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<$elementcount; 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 leftlocal 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 предоставил следующий сценарий, который демонстрирует операцию подстановки параметров.

Пример: 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/bash
# @(#) 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:
 
###;;;autoloa
dfunction 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 fileFollowed 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 
codeListArray() # 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 FILELOC
 
exit 0

Stephane Chazelas демонстрирует возможность объектно-ориентированного подхода к программированию в Bash-сценариях.

Пример: A-22. Объектно-ориентированная база данных.

#!/bin/bash
# obj-oriented.sh: Объектно-ориентрованный подход к программированию в сценариях.
# Автор: Stephane Chazelas.
 
person.new() # Очень похоже на объявление класса в C++.
{
 local obj_name=$1 name=$2 firstname=$3 birthdate=$4
 
 eval "$obj_name.set_name() {
  eval \"$obj_name.get_name() {
            echo \$1
         }\"
      }"
 eval "$obj_name.set_firstname() {
  eval \"$obj_name.get_firstname() {
            echo \$1
         }\"
       }"
 eval "$obj_name.set_birthdate() {
  eval \"$obj_name.get_birthdate() {
            echo \$1
          }\"
         eval \"$obj_name.show_birthdate() {
         echo \$(date -d \"1/1/1970 0:0:\$1 GMT\")
          }\"
         eval \"$obj_name.get_age() {
          echo \$(( (\$(date +%s) - \$1) / 3600 / 24 / 365 ))
         }\"
       }"
 
 $obj_name.set_name $name
 $obj_name.set_firstname $firstname
 $obj_name.set_birthdate $birthdate
}
 
echo
person.new self Bozeman Bozo 101272413
# Создаётся экземпляр класса "person.new" (фактически — вызов функции с аргументами).
self.get_firstname # Bozo
self.get_name # Bozeman
self.get_age # 28
self.get_birthdate # 101272413
self.show_birthdate # Sat Mar 17 20:13:33 MST 1973
 
echo
 
# typeset -f # чтобы просмотреть перечень созданных функций.
exit 0

Пример сценария, имеющего практическую ценность: установка и монтирование USB-устройств"жёстких дисков".

Пример: A-23. Монтирование USB-устройств флэш-памяти.

#!/bin/bash
# ==> usb.sh
# ==> Сценарий монтирует и устанавливает USB-устройства флэш-памяти.
# ==> Запускается с правами root во время загрузки системы (см. ниже).
 
# Этот сценарий распространяется на условиях GNU GPL license версии 2 или выше.
# Полный текст лицензии вы найдете на http://www.gnu.org/.
#
# Часть кода заимствована из сценария usb-mount, автор Michael Hamilton (LGPL)
# см. http://users.actrix.co.nz/michael/usbmount.html#
#
# УСТАНОВКА
# ---------
# Поместите сценарий в каталог /etc/hotplug/usb/diskonkey.
# Затем скопируйте все описания устройств usb-storage из /etc/hotplug/usb.distmap
# в /etc/hotplug/usb.usermap, заменяя "usb-storage" на "diskonkey".
#
# TODO
# ----
# Обслуживание более одного устройства diskonkey (например /dev/diskonkey1
# и /mnt/diskonkey1), и т.д. Наибольшая проблема здесь состоит в работе с
# devlabel.
#
# АВТОР и ПОДДЕРЖКА
# -------------------
# Konstantin Riabitsev, <icon linux duke edu>.
# Сообщения об обнаруженных ошибках отправляйте мне на электронный адрес.
#
# ==> Комментарии добавленные автором книги.
 
SYMLINKDEV=/dev/diskonkey
MOUNTPOINT=/mnt/diskonkey
DEVLABEL=/sbin/devlabel
DEVLABELCONFIG=/etc/sysconfig/devlabel
IAM=$0
 
##
# Функция заимствована из usb-mount.
#
 
function allAttachedScsiUsb {
 find /proc/scsi/ -path '/proc/scsi/usb-storage*' -type f | xargs grep -l 'Attached: Yes'
}
 
function scsiDevFromScsiUsb {
 echo $1 | awk -F"[-/]" '{ n=$(NF-1); print "/dev/sd" substr("abcdefghijklmnopqrstuvwxyz", n+1, 1) }'
}
 
if [ "${ACTION}" = "add" ] && [ -f "${DEVICE}" ]; then
##
# заимствовано из usbcam.
#
 if [ -f /var/run/console.lock ]; then
  CONSOLEOWNER=`cat /var/run/console.lock`
 elif [ -f /var/lock/console.lock ]; then
  CONSOLEOWNER=`cat /var/lock/console.lock`
 else
  CONSOLEOWNER=
 fi
 
 for procEntry in $(allAttachedScsiUsb); do
  scsiDev=$(scsiDevFromScsiUsb $procEntry)
  # Это ошибка в usb-storage?
  # Разделы не появляются в /proc/partitions до тех пор
  # пока к ним не было хотя бы одного обращения.
  /sbin/fdisk -l $scsiDev >/dev/null
  ##
  # Большинство устройств имеет информацию о разделах, например
  # /dev/sd?1. Однако, некоторые устройства не имеют разделов
  # выделяя под данные весь объем памяти. Здесь делается
  # попытка определить такие устройства, т.е. имеется ли /dev/sd?1
  # или нет.
  #
  if grep -q `basename $scsiDev`1 /proc/partitions; then
   part="$scsiDev""1"
  else
   part=$scsiDev
  fi
  ##
  # Изменение владельца устройства, чтобы пользователь
  # мог смонтировать его.
  #
  if [ ! -z "$CONSOLEOWNER" ]; then
   chown $CONSOLEOWNER:disk $part
  fi
  ##
  # Здесь проверяется — зарегистрирован ли UUID с помощью devlabel.
  # Если нет, то устройство добавляется в список.
  #
  prodid=`$DEVLABEL printid -d $part`
  if ! grep -q $prodid $DEVLABELCONFIG; then
   # скрестим пальцы, надеюсь это поможет
   $DEVLABEL add -d $part -s $SYMLINKDEV 2>/dev/null
  fi
  ##
  # Если точка монтирования отсутствует, то она создается.
  #
  if [ ! -e $MOUNTPOINT ]; then
   mkdir -p $MOUNTPOINT
  fi
  ##
  # Позаботиться о создании соответствующей записи в /etc/fstab.
  #
  if ! grep -q "^$SYMLINKDEV" /etc/fstab; then
   # Добавить запись в fstab
   echo -e \
   "$SYMLINKDEV\t\t$MOUNTPOINT\t\tauto\tnoauto,owner,kudzu 0 0" \
   >> /etc/fstab
  fi
 done
 
 if [ ! -z "$REMOVER" ]; then
  ##
  # Обеспечить запуск сценария при извлечении устройства.
  #
  mkdir -p `dirname $REMOVER`
  ln -s $IAM $REMOVER
 fi
 
elif [ "${ACTION}" = "remove" ]; then
 ##
 # Если устройство смонтировано — отмонтировать его.
 #
 if grep -q "$MOUNTPOINT" /etc/mtab; then
  # отмонтировать
  umount -l $MOUNTPOINT
 fi
 ##
 # Удалить запись из /etc/fstab, если она там имеется.
 #
 if grep -q "^$SYMLINKDEV" /etc/fstab; then
  grep -v "^$SYMLINKDEV" /etc/fstab > /etc/.fstab.new
  mv -f /etc/.fstab.new /etc/fstab
 fi
fi

А этот сценарий согреет душу и сердце и веб-мастера, и владельца сайта: он создаёт резервную копию файлов журналов WEB-сервера.

Пример: A-24. Резервное копирование файлов журналов.

#!/bin/bash
# archiveweblogs.sh v1.0
 
# Troy Engel <tengel@fluid.com>
# С небольшими изменениями, внесёнными автором документа.
# Используется с разрешения автора.
#
# Этот сценарий выполняет резервное копирование
# файлов журналов из стандартного каталога установки RedHat/Apache.
# Вставляет дату создания копии в имя файла-архива,
# сжимает (bzip), и помещает сжатые файлы в заданный каталог.
#
# Запускается из crontab ночью в 00 часов,
# так как bzip2 потребляет значительную часть ресурсов процессора,
# если журналы достаточно велики:
# 0 2 * * * /opt/sbin/archiveweblogs.sh
 
PROBLEM=66
# Здесь укажите ваш каталог для архивации.
BKP_DIR=/opt/backups/weblogs
 
# Настройки Apache/RedHat по-умолчанию
LOG_DAYS="4 3 2 1"
LOG_DIR=/var/log/httpd
LOG_FILES="access_log error_log"
 
# Расположение программ в RedHat по-умолчанию
LS=/bin/ls
MV=/bin/mv
ID=/usr/bin/id
CUT=/bin/cut
COL=/usr/bin/column
BZ2=/usr/bin/bzip2
 
# Проверка прав пользователя?
USER=`$ID -u`
if [ "X$USER" != "X0" ]; then
 echo "PANIC: Только root может запускать этот сценарий!"
 exit $PROBLEM
fi
 
# Каталог для резервной копии существует и доступен на запись?
if [ ! -x $BKP_DIR ]; then
 echo "PANIC: Каталог $BKP_DIR не найден или не доступен для записи!"
 exit $PROBLEM
fi
 
# Переместить, переименовать и сжать
for logday in $LOG_DAYS; do
 for logfile in $LOG_FILES; do
  MYFILE="$LOG_DIR/$logfile.$logday"
  if [ -w $MYFILE ]; then
   DTS=`$LS -lgo --time-style=+%Y%m%d $MYFILE | $COL -t | $CUT -d ' ' -f7`
   $MV $MYFILE $BKP_DIR/$logfile.$DTS
   $BZ2 $BKP_DIR/$logfile.$DTS
  else
   # Выдать сообщение об ошибке, если файл недоступен на запись.
   if [ -f $MYFILE ]; then
    echo "ERROR: $MYFILE not writable. Skipping."
   fi
  fi
 done
done
exit 0

Как предотвратить интерпретацию строки в сценарии?

Пример: A-25. Предотвращение интерпретации строк символов.

#! /bin/bash
# protect_literal.sh
# set -vx
:<<-'_Protect_Literal_String_Doc'
 Copyright (c) Michael S. Zick, 2003; All Rights Reserved
 Ограничения: Допускается использовать без каких либо ограничений в любой форме.
 Гарантии: Никаких
 Издание: $ID$
 
 Этот встроенный документ Bash отправит на устройство '/dev/null'.
 (Раскомментарьте команду set, стоящую выше, чтобы убедиться в этом.)
 
 Удалите первую строку (Sha-Bang), если вы собираетесь использовать этот сценарий
 в качестве библиотеки. Не забудьте при этом закомментарить примеры
 использования процедур (там где это указано).
 
 Порядок использования:
  _protect_literal_str 'Whatever string meets your ${fancy}'
  Какая бы строка ни была передана функции,
  она просто будет выведена на stdout,
  включая "строгие" кавычки.
 
  $(_protect_literal_str 'Whatever string meets your ${fancy}')
  как правосторонняя часть операции присваивания.
 
 Назначение:
  В операциях присваивания, предотвращают дополнительную
  интерпретацию содержимого строки, путем добавления "строгих" кавычек.
 
 Примечание:
  Имена функций (_*) выбраны таким образом, чтобы избежать
  конфликтов имён, при подключении данного сценария
  к пользовательским сценариям, в качестве библиотеки.
 
_Protect_Literal_String_Doc
 
_protect_literal_str() {
 # Выберем неиспользуемый, непечатный символ в качестве разделителя полей для IFS.
 # В этом нет необходимости, но делается это для демонстрации
 # того, что разделитель полей игнорируется.
 local IFS=$'\x1B' # Символ \ESC
 # Заключим Все-Элементы в "строгие" кавычки.
 local tmp=$'\x27'$@$'\x27'
 
 local len=${#tmp} # Исключительно для демонстрации.
 echo $tmp, длина:$len. # Вывод строки и дополнительной информации.
}
 
# Версия с более коротким именем.
_pls() {
 local IFS=$'x1B'      # Символ \ESC (не обязательно)
 echo $'\x27'$@$'\x27' # Заключить в "строгие" кавычки
}
 
# :<<-'_Protect_Literal_String_Test'
# # # Раскомментарьте вышестоящую строку, чтобы запретить исполнение нижеследующего кода. # # #
 
# Посмотрим как выглядит простой вывод.
echo
echo "- - Тест #1 - -"
 
_protect_literal_str 'Hello $user'
_protect_literal_str 'Hello "${username}"'
echo
 
# В результате должно получиться:
# - - Тест #1 - -
# 'Hello $user', длина: 13.
# 'Hello "${username}"', длина: 21.
 
# Собственно получили то, что и ожидали, тогда в чем проблема?
# Проблема скрыта внутри Bash, в порядке выполнения операций.
# Она проявляется, когда функции участвуют в операциях присваивания.
 
# Объявим массив тестовых значений.
declare -a arrayZ
 
# Запишем в массив элементы с разного рода кавычками и экранирующими символами.
arrayZ=( zero "$(_pls 'Hello ${Me}')" 'Hello ${You}' "\'Pass: ${pw}\'" )
 
# Теперь выведем массив на экран и посмотрим, что там лежит.
echo "- - Тест #2 - -"
for (( i=0 ; i<${#arrayZ[*]} ; i++ ))
do
 echo Элемент $i: ${arrayZ[$i]}, длина: ${#arrayZ[$i]}.
done
echo
 
# В результате должно получиться:
# - - Тест #2 - -
# Элемент 0: zero, длина: 4. 
# Маркировочный (ничем не примечательный) элемент
# Элемент 1: 'Hello ${Me}', длина: 13. # Результат "$(_pls '...' )"
# Элемент 2: Hello ${You}, длина: 12. # Кавычки исчезли
# Элемент 3: \'Pass: \', длина: 10. # ${pw} -- была интерпретирована,
#                                   # а на её место подставлена пустая строка
 
# Выполним присвоение одного массива другому.
declare -a array2=( ${arrayZ[@]} )
 
# И выведем его содержимое.
echo "- - Тест #3 - -"
for (( i=0 ; i<${#array2[*]} ; i++ ))
do
 echo Элемент $i: ${array2[$i]}, длина: ${#array2[$i]}.
done
echo
 
# В результате должно получиться:
# - - Тест #3 - -
# Элемент 0: zero, длина: 4.         # Наш маркер.
# Элемент 1: Hello ${Me}, длина: 11. # Вполне предсказуемый результат.
# Элемент 2: Hello, длина: 5.        # ${You} -- была интерпретирована.
#                                    # а на её место подставлена пустая строка
# Элемент 3: 'Pass:, длина: 6.       # Элемент был "разбит" на два по пробелу.
# Элемент 4: ', длина: 1.            # Завершающая кавычка попала в отдельный элемент.
 
# В Элементе 1 были удалены начальная и завершающая "строгие" кавычки.
# Хотя здесь и не показано, но начальные и завершающие пробелы также удаляются.
# Теперь, когда содержимое строки установлено, Bash всегда, внутри, будет
# "строго" окавычивать содержимое строки, на протяжении всей операции
 
# Зачем это нужно?
# В нашем случае, в конструкции "$(_pls 'Hello ${Me}')":
# " ... " -> Требуется интерпретация (экспансия), кавычки удаляются.
# $( ... ) -> Замещается результатом выполнения ..., пустая строка.
# _pls ' ... ' -> вызов функции со строковым аргументом, кавычки удаляются.
# Возвращаемый результат включает в себя "строгие" кавычки; НО обработка команды
# уже была завершена выше, так что теперь они становятся частью присваиваемого значения.
#
# Таким образом, ${Me} оказывается частью результата
# и сохраняется в первоначальном виде
# (До тех пор, пока явно не будет указано на необходимость ее интерпретации).
 
# Дополнительно: Взгляните, что произойдет, если в этих функциях
# "строгие" кавычки ($'\x27') заменить на "мягкие" ($'\x22').
# Интересный результат получится если вообще убрать кавычки.
 
# _Protect_Literal_String_Test
# # # Раскомментарьте вышестоящую строку, чтобы запретить исполнение вышестоящего кода. # # #
 
exit 0

А что делать, если необходимо заставить командный интерпретатор интерпретировать строку?

Пример: A-26. Принудительная интерпретация строк.

#! /bin/bash
# unprotect_literal.sh
 
# set -vx
 
:<<-'_UnProtect_Literal_String_Doc'
 Copyright (c) Michael S. Zick, 2003; All Rights Reserved
 Ограничения: Допускается использовать без каких либо ограничений в любой форме.
 Гарантии: Никаких
 Издание: $ID$
 
 Этот встроенный документ Bash отправит на устройство '/dev/null'.
 (Раскомментарьте команду set, стоящую выше, чтобы убедиться в этом.)
 
 Удалите первую строку (Sha-Bang), если вы собираетесь использовать этот сценарий
 в качестве библиотеки. Не забудьте при этом закомментарить примеры
 использования процедур (там где это указано).
 
 Порядок использования:
  Противоположная по смыслу функции "$(_pls 'Literal String')".
  (см. пример protect_literal.sh)
 
  StringVar=$(_upls ProtectedSringVariable)
 
 Назначение:
  Выполняет подстановку (интерпретацию) строк в операциях присваивания.
 
 Примечание:
  Имена функций (_*) выбраны таким образом, чтобы избежать
  конфликтов имен, при подключении данного сценария
  к пользовательским сценариям, в качестве библиотеки.
 
_UnProtect_Literal_String_Doc
 
_upls() {
 local IFS=$'x1B'                 # Символ \ESC character (не обязательно)
 eval echo $@                     # Принудительная интерпретация.
}
 
# :<<-'_UnProtect_Literal_String_Test'
# # # Раскомментарьте вышестоящую строку, чтобы запретить исполнение нижеследующего кода. # # #
 
_pls() {
 local IFS=$'x1B' # Символ \ESC character (не обязательно)
 echo $'\x27'$@$'\x27' # Заключить в "строгие" кавычки
}
# Объявим массив тестовых значений.
declare -a arrayZ
 
# Запишем в массив элементы с разного рода кавычками и экранирующими символами.
arrayZ=( zero "$(_pls 'Hello ${Me}')" 'Hello ${You}' "\'Pass: ${pw}\'" )
 
# Выполним присвоение одного массива другому.
declare -a array2=( ${arrayZ[@]} )
 
# В результате должно получиться:
# - - Тест #3 - -
# Элемент 0: zero, длина: 4.          # Наш маркер.
# Элемент 1: Hello ${Me}, длина: 11.  # Вполне предсказуемый результат.
# Элемент 2: Hello, длина: 5.         # ${You} — была интерпретирована.
#                                     # а на её место подставлена пустая строка
# Элемент 3: 'Pass:, длина: 6.        # Элемент был "разбит" на два по пробелу.
# Элемент 4: ', длина: 1.             # Завершающая кавычка попала в отдельный элемент.
 
# set -vx
 
# Инициализируем переменную 'Me' каким-нибудь значением
# чтобы увидеть последующую её интерпретацию.
 
Me="to the array guy."
 
# Присвоим результат принудительной интерпретации другой переменной.
newVar=$(_upls ${array2[1]})
 
# И посмотрим, что получилось.
echo $newVar
 
# Так ли необходима эта функция?
newerVar=$(eval echo ${array2[1]})
echo $newerVar
 
# Оказывается совсем не обязательно, но функция _upls делает сценарий
# более понятным.
 
# Она поможет в том случае, если вдруг забудется смысл конструкции
# $(eval echo ... ).
 
# Что произойдет, если часть строки,
# которая требует дополнительной интерпретации, окажется неинициализированной?
unset Me
newestVar=$(_upls ${array2[1]})
echo $newestVar
 
# Просто и со вкусом! Никаких сообщений, никаких предупреждений, никаких ошибок.
 
# Для чего все это?
# Одна из основных проблем в Bash — невозможность записать в переменные
# некоторые последовательности символов
#
# Теперь эта проблема разрешается восемью строчками кода
# (и четырьмя страницами описания).
 
# Где это можно использовать?
# Для динамической генерации содержимого Web-страниц,
# в виде массивов строк.
 
# Содержимое таких страниц может генерироваться командой Bash 'eval'
# Я совсем не призываю заменить PHP, просто высказал интересную мысль.
###
 
# UnProtectLiteralStringTest
 
# # # Раскомментарьте вышестоящую строку, чтобы запретить исполнение вышестоящего кода. # # #
 
exit 0

В завершение этого раздела, бросим краткий взгляд назад...

Пример: A-27. Повторение основ.

#!/bin/bash
# basics-reviewed.bash
 
# Расширение файла == *.bash == сценарий, использующий особенности Bash
 
# Copyright (c) Michael S. Zick, 2003; All rights reserved.
# License: Use in any form, for any purpose.
# Издание: $ID$
#
# Правка, с целью улучшения оформления, выполнена автором книги.
# ("Advanced Bash Scripting Guide")
 
# Этот сценарий тестировался в Bash версий 2.04, 2.05a и 2.05b.
# Он может не работать в более ранних версиях.
# Этот сценарий умышленно генерирует ошибку
# "command not found". См. строку 394.
 
# Ведущий разработчик Bash, Chet Ramey, обязался исправить эту проблему
# в следующих версиях Bash.
 
###-------------------------------------------###
### Сценарий выводит много информации на      ###
### экран, поэтому запускайте его в конвейере ###
### с командой more                           ###
###                                           ###
### Кроме того, вы можете перенаправить       ###
### вывод в файл, с целью последующего        ###
### изучения                                  ###
###-------------------------------------------###
 
# Большая часть из приводимых здесь моментов описывается
# в вышеупомянутой книге "Advanced Bash Scripting Guide."
# Этот сценарий, по сути можно расценивать как своего рода презентацию.
# -- msz
 
# Переменные не типизированы, если не указано обратное.
 
# Соглашения по именованию переменных.
# Имена переменных не должны начинаться с цифровых символов.
# Имена дескрипторов файлов (как например: 2>&1)
# должны содержать ТОЛЬКО цифры.
 
# Параметры и элементы массивов Bash — пронумерованы.
# (Параметры, в этом смысле, очень похожи на массивы.)
 
# Переменные в Bash могут иметь неопределенное значение.
unset VarNull
 
# Переменные в Bash могут быть определены, но содержать "пустое" (null) значение.
VarEmpty=''
 
# Переменные могут быть определены и содержать некоторое, непустое значение
VarSomething='Literal'
 
# Переменные могут хранить:
# * Целое 32-битовое число со знаком
# * Строку символов
# Переменные могут быть массивом.
 
# Строки могут содержать пробелы и интерпретироваться
# как вызов функции с аргументами.
 
# Имена переменных и имена функций
# находятся в разных пространствах имен (namespaces).
 
# Переменные могут быть объявлены массивами как явно, так и неявно
# в зависимости от семантики операции присваивания.
# Явно:
declare -a ArrayVar
# Команда echo — внутренняя команда.
 
echo $VarSomething
# Команда printf — внутренняя команда.
 
# здесь %s интерпретируется как строка формата
printf %s $VarSomething # Перевод строки отсутствует, ничего не выводится.
echo # Выводит только перевод строки.
 
# Интерпретатор Bash различает отдельные слова по символу пробела между ними.
# Поэтому наличие или отсутствие пробела — очень важный признак.
# (В общем случае это так, но есть и исключения из правил.)
 
# Символ "доллара" ($) интерпретируется как: Content-Of (содержимое для ...).
 
# Расширенный синтаксис, с использованием символа "доллара":
echo ${VarSomething}
 
# Здесь, конструкция ${ ... }, позволяет указывать
# не только имена переменных.
# Как правило, запись $VarSomething
# всегда может быть представлена в виде : ${VarSomething}.
 
# Чтобы увидеть следующие операции в действии — вызовите сценарий
# с несколькими входными аргументами.
 
# За пределами двойных кавычек, специальные символы @ и *
# имеют идентичное назначение.
# Может произноситься как: All-Elements-Of (Все-Элементы-Для).
 
# Если имя переменной не указано, то эти специальные символы
# применяются к предопределенным переменным Bash.
 
echo $* # Все входные параметры сценария или функции
echo ${*} # То же самое
# Bash запрещает подстановку имён файлов в вышеупомянутых конструкциях.
 
# Ссылка на Все-Элементы-Для
echo $@ # то же самое, что и выше
echo ${@} # то же самое
 
# Внутри двойных кавычек, поведение символов @ и *
# зависит от установки переменной IFS (Input Field Separator — Разделитель Полей).
# Ссылка на Все-Элементы-Для, внутри двойных кавычек, работает точно так же.
 
# Обращение к имени переменной означает получение
# всех элементов (символов) строки.
 
# Для обращения к отдельным элементам (символам) строки,
# может использоваться расширенный синтаксис (см. ниже).
 
# Обращение к имени переменной-массива в Bash
# означает обращение к нулевому элементу массива,
# а НЕ к ПЕРВОМУ ОПРЕДЕЛЕННОМУ или к ПЕРВОМУ ИНИЦИАЛИЗИРОВАННОМУ элементу.
 
# Для обращения к другим элементам массива, необходимо явное указание элемента,
# это означает, что ДОЛЖЕН использоваться расширенный синтаксис.
# В общем случае: ${name[subscript]}.
 
# Для строк может использоваться такая форма записи: ${name:subscript},
# а также для обращения к нулевому элементу массива.
 
# Массивы в Bash реализованы как связанные списки,
# а не как фиксированная область памяти, что характерно для некоторых 
# языков программирования.
 
# Характеристики массивов в Bash:
# -------------------------------
 
# Если не определено иначе, индексация массивов в Bash
# начинается с нуля: [0]
# Это называется "индексация с нуля".
###
# Если не указано иначе, массивы в Bash являются упакованными
# (т.е. массивы просто не содержат элементов с отсутствующими индексами).
###
# Отрицательные индексы недопустимы.
###
# Элементы массива не обязательно должны быть одного и того же типа.
###
# Элементы массива могут быть неинициализированы.
# Т.е. массив может быть "разреженным"
###
# Элементы массива могут быть инициализированы пустым значением.
###
# Элементы массива могут содержать:
# * Целое 32-битовое число со знаком
# * Строку
# * Форматированную строку, которая выглядит
#   как вызов к функции с параметрами
###
# Инициализированные элементы массива могут быть деинициализированы (unset).
# Т.е. массив может быть переупакован так,
# что он не будет содержать элемента с данным индексом.
###
# К массиву могут добавляться дополнительные элементы,
# не определенные ранее.
###
# По этим причинам я называю массивы Bash — "Bash-массивами" ("Bash-Arrays").
#
# -- msz
 
# Демонстрация вышесказанного — инициализируем ранее объявленный массив ArrayVar
# как "разреженный" массив.
# (команда 'unset ... ' используется для демонстрации вышесказанного.)
 
unset ArrayVar[0]                  # Для демонстрации
ArrayVar[1]=one                    # Без кавычек
ArrayVar[2]=''                     # Инициализация пустым значением
unset ArrayVar[3]                  # Для демонстрации
ArrayVar[4]='four'                 # В кавычках
 
# Строка формата %q — трактуется как: Quoted-Respecting-IFS-Rules
# (в соответствии с установками IFS).
 
echo
echo '- - Вне двойных кавычек - -'
###
printf %q ${ArrayVar[*]}           # Шаблон "Все-Элементы-Для"
echo
echo 'команда echo:'${ArrayVar[*]}
###
printf %q ${ArrayVar[@]}           # "Все-Элементы-Для"
echo
echo 'команда echo:'${ArrayVar[@]}
 
# Двойные кавычки используются для разрешения операции подстановки
# внутри кавычек.
# Существует пять самых распространенных случаев,
# зависящих от установок переменной IFS.
 
echo
 
echo '- - В двойных кавычках - По-умолчанию IFS содержит пробел-табуляцию-перевод строки- -'
IFS=$'\x20'$'\x09'$'\x0A'           # Три байта, и именно в таком порядке.
 
printf %q "${ArrayVar[*]}"          # Шаблон "Все-Элементы-Для"
echo
echo 'команда echo:'"${ArrayVar[*]}"
###
printf %q "${ArrayVar[@]}"          # "Все-Элементы-Для"
echo
echo 'команда echo:'"${ArrayVar[@]}"
 
echo
echo '- - В двойных кавычках - Первый символ в IFS: ^ - -'
# Любой печатаемый, непробельный символ, дает тот же эффект.
IFS='^'$IFS                         # ^ + пробел табуляция перевод строки###
printf %q "${ArrayVar[*]}"          # Шаблон "Все-Элементы-Для"
echo
echo 'команда echo:'"${ArrayVar[*]}"
###
printf %q "${ArrayVar[@]}"          # "Все-Элементы-Для"
echo
echo 'команда echo:'"${ArrayVar[@]}"
 
echo
echo '- - В двойных кавычках - IFS не содержит пробела - -'
IFS='^:%!'
###
printf %q "${ArrayVar[*]}"          # Шаблон "Все-Элементы-Для"
echo
echo 'команда echo:'"${ArrayVar[*]}"
###
printf %q "${ArrayVar[@]}"          # "Все-Элементы-Для"
echo
echo 'команда echo:'"${ArrayVar[@]}"
 
echo
echo '- - В двойных кавычках - переменная IFS пуста - -'
IFS=''
###
printf %q "${ArrayVar[*]}"           # Шаблон "Все-Элементы-Для"
echo
echo 'команда echo:'"${ArrayVar[*]}"
###
printf %q "${ArrayVar[@]}"           # "Все-Элементы-Для"
echo
echo 'команда echo:'"${ArrayVar[@]}"
 
echo
echo '- - В двойных кавычках - переменная IFS не определена - -'
unset IFS
###
printf %q "${ArrayVar[*]}"           # Шаблон "Все-Элементы-Для" All-Elements-Of
echo
echo 'команда echo:'"${ArrayVar[*]}"
###
printf %q "${ArrayVar[@]}"           # "Все-Элементы-Для"
echo
echo 'команда echo:'"${ArrayVar[@]}"
 
# Вернём переменную IFS в первоначальное состояние,
# записав в неё значение по-умолчанию.
 
IFS=$'\x20'$'\x09'$'\x0A'             # точно в таком порядке.
 
# Интерпретация результатов, полученных выше:
# Форма ввода по шаблону "Все-Элементы-Для" зависит от содержимого переменной
###
# Простой вывод "Всех-Элементов-Для" не зависит от содержимого переменной IFS.
###
# Обратите внимание на различия, имеющиеся в выводе         
# от команд echo и printf с форматом %q.
 
# Давайте вспомним:
# Параметры очень похожи на массивы и имеют сходное поведение.
###
# Примеры выше показывают, что для того, чтобы вывести разреженный
# массив полностью, необходимо писать дополнительный код.
###
 
# Длина строки равна количеству ненулевых элементов (символов):
echo
echo '- - Имя переменной употребляется вне кавычек - -'
echo 'Количество ненулевых символов: '${#VarSomething}'.'
 
# test='Lit'$'\x00''eral'             # $'\x00' — нулевой (null) символ.
# echo ${#test}                       # Что получится?
 
# Длина массива равна количеству инициализированных элементов,
# включая элементы, инициализированные пустыми значениями.
echo
echo 'Количество инициализированных элементов в массиве: '${#ArrayVar[@]}'.'
# Это НЕ максимальный индекс массива (4).
# Это НЕ ширина диапазона (1 . . 4 включительно).
# Это длина связного списка.
###
# Максимальный номер индекса массива и диапазон индексов
# могут быть найдены, но для этого потребуется дополнительный код.
 
# Длина строки равна количеству ненулевых элементов (символов):
echo
echo '- - Имя переменной употребляется в кавычках - -'
echo 'Количество непустых символов: '"${#VarSomething}"'.'
 
# Длина массива равна количеству инициализированных элементов,
# включая элементы, инициализированные пустыми значениями.
echo
echo 'Количество инициализированных элементов в массиве: '"${#ArrayVar[*]}"'.'
 
# Вывод: Конструкция ${# ... } не производит подстановку.
# Совет:
# Всегда используйте символ All-Elements-Of (Все-Элементы-Для)
# если желаете получить результат, не зависящий от содержимого переменной IFS.
 
# Определим простую функцию.
# Я включил в имя функции символ подчеркивания
# чтобы как-то обозначить, что это функция, а не переменная.
###
# Bash различает имена функций и переменных,
# размещая из в различных пространствах имён.
###
 
_simple() {
 echo -n 'SimpleFunc'$@                 # Символ перевода строки в любом случае
}                                       # будет "съеден".
# Конструкция ( ... ) вызывает команду или функцию.
# Форма записи $( ... ) произносится как: Result-Of (Результат-Выполнения).
 
# Вызовем функцию _simple
echo
echo '- - Результат работы функции _simple - -'
 
_simple                                 # Попробуйте передать несколько аргументов.
echo
# или
(_simple)                               # Попробуйте передать несколько аргументов.
echo
 
echo '- Существует ли переменная с таким именем? -'
echo $_simple not defined # Нет переменной с таким именем.
 
# Обращение к результату выполнения функции _simple
# (будет получено сообщение об ошибке)
###
$(_simple)                              # Генерирует сообщение об ошибке:
# line 394: SimpleFunc: command not found
# ---------------------------------------
echo
###
 
# Причина ошибки вполне очевидна: результат работы функции _simple не есть
# ни команда Bash, ни имя определенной ранее функции.
###
# Этот пример показывает, что вывод от функции _simple подвергается
# дополнительной интерпретации.
###
# Вывод:
# Функция может использоваться для генерации команд Bash.
 
# Простая функция, которая выводит команду bash:
###
_print() {
 echo -n 'printf %q '$@
}
 
echo '- - Результат работы функции _print - -'
_print parm1 parm2                       # Простой вывод — НЕ команда.
echo
$(_print parm1 parm2)                    # Исполняет команду printf %q parm1 parm2
                                         # См. пример с IFS выше
                                         # на предмет дополнительных возможностей.
echo
$(_print $VarSomething)                  # Вполне предсказуемый результат.
echo
 
# Переменные-функции
# ------------------
 
echo
echo '- - Переменные-функции - -'
# Переменная может хранить целое число, строку или массив.
# Строка может интерпретироваться как вызов функции.
 
# set -vx                               # Раскомментарьте при желании
 
declare -f funcVar                      # в пространстве имён функций!
 
funcVar=_print                          # Записать имя функции.
$funcVar parm1                          # Аналогично вызову функции _print.
 
echo
 
funcVar=$(_print )                      # Результат работы функции.
$funcVar                                # Нет ни ввода, ни вывода.
$funcVar $VarSomething                  # Предсказуемый результат.
echo
 
funcVar=$(_print $VarSomething)         # Здесь выполняется подстановка
                                        # значения переменной $VarSomething.
 
$funcVar                                # Содержимое переменной $VarSomething
echo                                    # стало частью переменной $funcVar
 
funcVar="$(_print $VarSomething)"       # Здесь выполняется подстановка
                                        # значения переменной $VarSomething.
 
$funcVar                                # Содержимое переменной $VarSomething
echo                                    # стало частью переменной $funcVar
 
# Различия в применении или неприменении двойных кавычек
# объясняются в примере "protect_literal.sh".
# В первом случае Bash обрабатывает строку как два отдельных слова,
# во втором — как одно слово в кавычках с пробелом внутри слова.
 
# Отложенная подстановка
# ----------------------
 
echo
echo '- - Отложенная подстановка - -'
funcVar="$(_print '$VarSomething')"       # Подстановка значения переменной не производится.
eval $funcVar                             # Подстановка производится ЗДЕСЬ.
echo
 
VarSomething='NewThing'
eval $funcVar                             # Подстановка производится ЗДЕСЬ.
echo
 
# Восстановим прежнее значение переменной VarSomething.
VarSomething=Literal
# В примерах "protect_literal.sh" и "unprotect_literal.sh"
# вы найдёте две функции, которые выполняют отложенную подстановку
# значений переменных.
 
# ОБЗОР:
# -----
 
# Строки могут рассматриваться как классический массив элементов-символов.
# Строковые операции воздействуют на все элементы (символы) строки
###
# Запись: ${array_name[@]} представляет все элементы
# Bash-Массива: array_name.
###
# Строковые операции, в расширенном синтаксисе, могут манипулировать
# всеми элементами массива сразу.
###
# Эта способность может рассматриваться как операция For-Each над вектором строк.
###
# Параметры подобны массивам.
# Различия в параметрах для функции и сценария касаются только параметра
# ${0}, который никогда не изменяется.
###
# Нулевой параметр сценария содержит имя файла сценария
###
# Нулевой параметр функции НЕ СОДЕРЖИТ имени функции.
# Имя функции хранится в служебной переменной $FUNCNAME.
###
 
echo
echo '- - Тест (без изменения содержимого переменной) - -'
echo '- неинициализированная переменная -'
echo -n ${VarNull-'NotSet'}' '            # NotSet
echo ${VarNull}                           # только перевод строки
echo -n ${VarNull:-'NotSet'}' '           # NotSet
 
echo ${VarNull}                           # только перевод строкиecho '- "пустая" переменная -'
echo -n ${VarEmpty-'Empty'}' '            # только пробел
echo ${VarEmpty}                          # только перевод строки
echo -n ${VarEmpty:-'Empty'}' '           # Empty
echo ${VarEmpty}                          # только перевод строки
 
echo '- непустая переменная -'
echo ${VarSomething-'Content'}            # Literal
echo ${VarSomething:-'Content'}           # Literal
 
echo '- Разреженный массив -'
echo ${ArrayVar[@]-'not set'}
 
# ASCII-Art      time
#      State Y==yes, N==no
#            -       :-
# Unset      Y       Y        ${# ... } == 0
# Empty      N       Y        ${# ... } == 0
# Contents   N       N        ${# ... } > 0
 
# Либо первая, либо вторая часть операции подстановки параметра по-умолчанию
# может быть командой или вызовом функции.
echo
echo '- - Тест 1, не определенная переменная - -'
declare -i t
_decT() {
 t=$t-1
}
# Для не определенной переменной: t == -1
t=${#VarNull}                  # t == 0
${VarNull- _decT }             # Вызов функции, теперь t == -1.
echo $t
 
# "Пустая" переменная: t == 0
t=${#VarEmpty}                 # t == 0
${VarEmpty- _decT }            # Функция _decT НЕ вызывается.
echo $t
 
# Непустая переменная: t == число непустых символов
VarSomething='_simple'         # Записать имя функции в переменную.
t=${#VarSomething}             # ненулевая длина
${VarSomething- _decT }        # Вызывается функция _simple.
echo $t                        # Обратите внимание на вывод.
 
# Упражнение: Разберитесь в этом примере.
unset t
unset _decT
VarSomething=Literal
 
echo
echo '- - Тест (с изменением содержимого переменной) - -'
echo '- Присвоить, если переменная не определена -'
echo -n ${VarNull='NotSet'}' '  # NotSet NotSet
echo ${VarNull}
unset VarNull
 
echo '- Присвоить, если переменная не определена -'
echo -n ${VarNull:='NotSet'}' ' # NotSet NotSet
echo ${VarNull}
unset VarNull
 
echo '- Не присваивать, если переменная пуста -'
echo -n ${VarEmpty='Empty'}' '  # только пробел
echo ${VarEmpty}
VarEmpty=''
 
echo '- Присвоить, если переменная пуста -'
echo -n ${VarEmpty:='Empty'}' ' # Empty Empty
echo ${VarEmpty}
VarEmpty=''
 
echo '- Не изменять, если переменная не пуста -'
echo ${VarSomething='Content'}  # Literal
echo ${VarSomething:='Content'} # Literal
 
# "Разреженные" Bash-Массивы
###
# Bash-Массивы не содержат пустых элементов, и индексация их,
# если не оговорено иное, начинается с нуля.
###
# Инициализируем массив ArraySparse как раз тем способом,
# когда "оговорено иное". Ниже приведен один из вариантов:
###
echo
declare -a ArraySparse
ArraySparse=( [1]=one [2]='' [4]='four' )
# [0]=нет элемента, [2]=пустой элемент, [3]=нет элемента
 
echo '- - Разреженный массив - -'
# В двойных кавычках, значение IFS -- по-умолчанию, Все-Элементы-Для
 
IFS=$'\x20'$'\x09'$'\x0A'printf %q "${ArraySparse[*]}"
echo
 
# Обратите внимание на отсутствие различий в том, как выводятся отсутствующий
# и пустой элементы массива.
# Оба выводятся как экранированные пробелы.
###
# Не упустите из виду и то, что все неопределенные элементы массива,
# предшествующие первому инициализированному элементу, не выводятся
###
# Замечание о таком "поведении" Bash, версий 2.04, 2.05a и 2.05b,
# было передано разработчикам has been reported
# и возможно будет изменено в последующих версиях Bash.
 
# Чтобы вывести содержимое такого разреженного массива без изменений
# требуется некоторое количество усилий.
# Вот один из возможных вариантов вывода такого массива:
###
# local l=${#ArraySparse[@]}       # Количество инициализированных элементов
# local f=0                        # Количество найденных индексов
# local i=0                        # текущий индекс
 
(                                  # Анонимная функция
 for (( l=${#ArraySparse[@]}, f = 0, i = 0 ; f < l ; i++ ))
 do
  # 'if defined then...'
  ${ArraySparse[$i]+ eval echo '\ ['$i']='${ArraySparse[$i]} ; (( f++ )) }
 done
)
 
# Важно:
# Команда "read -a array_name" начинает заполнять массив
# array_name с нулевого элемента.
# ArraySparse -- не содержит нулевого элемента.
###
# Для выполнения операций над разреженными массивами,
# такими как чтение/запись массива из/в файла
# программист должен сам создать программный код, который
# будет удовлетворять его потребности.
###
# Упражнение: разберитесь в следующем примере самостоятельно.
 
unset ArraySparse
 
echo
echo '- - Замена по условию (замена не производится)- -'
echo '- Не изменять если переменная не определена -'
echo -n ${VarNull+'NotSet'}' '
echo ${VarNull}
unset VarNull
 
echo '- Не изменять если переменная не определена -'
echo -n ${VarNull:+'NotSet'}' '
echo ${VarNull}
unset VarNull
 
echo '- Изменить если переменная пуста -'
echo -n ${VarEmpty+'Empty'}' ' # Empty
echo ${VarEmpty}
VarEmpty=''
 
echo '- Не изменять если переменная пуста -'
echo -n ${VarEmpty:+'Empty'}' '          # Только пробел 
echo ${VarEmpty}
VarEmpty=''
 
echo '- Изменить, если переменная не пуста -'
 
echo -n ${VarSomething+'Content'}' '     # Content Literal
echo ${VarSomething}
 
# Вызов функции
echo -n ${VarSomething:+ $(_simple) }' ' # SimpleFunc Literal
echo ${VarSomething}
echo
echo '- - Разреженный массив - -'
echo ${ArrayVar[@]+'Empty'}              # An array of 'Empty'(ies)
echo
 
echo '- - Тест 2, неопределенные переменные - -'
 
declare -i t
_incT() {
 t=$t+1
}
 
# Обратите внимание:
# Тот же самый тест, что и в случае с разреженными массивами
 
# Неопределенная переменная: t == -1
t=${#VarNull}-1                        # t == -1
${VarNull+ _incT }                     # Функция не вызывается.
 
echo $t' Переменная не определена'
 
# Пустая переменная: t == 0
t=${#VarEmpty}-1                       # t == -1
${VarEmpty+ _incT }                    # Вызов функции.
echo $t' Переменная пуста'
# Переменная не пуста: t == (количество непустых символов)
t=${#VarSomething}-1                   # количество_непустых_символов минус один
${VarSomething+ _incT }                # Вызов функции.
echo $t' Переменная не пуста'
 
# Операции над элементами массива
# -------------------------------
 
echo
echo '- - Выборка элементов - -'
 
# Строки, массивы и позиционные параметры
# Чтобы увидеть работу сценария с позиционными параметрами,
# вызовите его с несколькими аргументами
 
echo '- Все -'
echo ${VarSomething:0}                  # все не пустые символы
echo ${ArrayVar[@]:0}                   # все не пустые элементы
echo ${@:0}                             # все не пустые параметры;
                                        # параметр [0] игнорируется
echo
echo '- Все после -'
echo ${VarSomething:1}                  # все не пустые символы, стоящие после [0]
echo ${ArrayVar[@]:1}                   # все не пустые элементы, стоящие после [0]
echo ${@:2}                             # все не пустые параметры, стоящие после [1]
 
echo
echo '- Диапазон символов -'
echo ${VarSomething:4:3}                # ral 
                                        # Три символа, следующие за символом [3]
echo '- Разреженный массив -'
echo ${ArrayVar[@]:1:2}                 # four - Единственный непустой элемент.
 
# Чтобы убедиться в том, что Bash в данной ситуации рассматривает только
# непустые элементы
 
# printf %q "${ArrayVar[@]:0:3}"        # Попробуйте раскомментарить эту строку
 
# Версии Bash 2.04, 2.05a и 2.05b,
# работают с разреженными массивами не так как ожидается.
#
# Chet Ramey обещал исправить это в последующих версиях Bash.
 
echo '- Неразреженный массив -'
echo ${@:2:2}                           # За параметром [1] следуют ещё два параметра
 
# Простые примеры со строками и массивами строк:
stringZ=abcABC123ABCabc
arrayZ=( abcabc ABCABC 123123 ABCABC abcabc )
sparseZ=( [1]='abcabc' [3]='ABCABC' [4]='' [5]='123123' )
 
echo
echo ' - - Простая строка - -'$stringZ'- - '
echo ' - - Простой массив - -'${arrayZ[@]}'- - '
echo ' - - Разреженный массив - -'${sparseZ[@]}'- - '
echo ' - [0]==нет элемента, [2]==нет элемента, [4]==пустой элемент - '
echo ' - [1]=abcabc [3]=ABCABC [5]=123123 - '
echo ' - количество инициализированных элементов: '${#sparseZ[@]}
 
echo
echo '- - Префиксы - -'
echo '- - Шаблон должен совпадать с первым символом строки. - -'
echo '- - Шаблон может быть строкой или результатом работы функции. - -'
echo
 
# Функция, результатом работы которой является обычная строка
_abc() {
 echo -n 'abc'
}
 
echo '- Кратчайший префикс -'
echo ${stringZ#123}                   # Без изменения — не префикс.
echo ${stringZ#$(abc)}                # ABC123ABCabc
echo ${arrayZ[@]#abc}                 # Применяется к каждому элементу массива.
 
# Chet Ramey обещал исправить в последующих версиях Bash.
# echo ${sparseZ[@]#abc}              # В версии 2.05b — core dumps.
 
# -Это было бы здорово- First-Subscript-Of (Первый-Индекс-Массива)
# echo ${#sparseZ[@]#*} # line 805: ${#sparseZ[@]#*}: bad substitution.
 
echo
echo '- Наибольший префикс -'
echo ${stringZ##1*3}                  # Без изменения — не префикс.
echo ${stringZ##a*C}                  # abc
echo ${arrayZ[@]##a*c}                # ABCABC 123123 ABCABC
# Chet Ramey обещал исправить в последующих версиях Bash.
# echo ${sparseZ[@]##a*c}             # В версии 2.05b — core dumps.
 
echo
echo '- - Суффиксы - -'
echo '- - Шаблон должен совпадать с последним символом строки. - -'
echo '- - Шаблон может быть строкой или результатом работы функции. - -'
echo
echo '- Кратчайший суффикс -'
echo ${stringZ%1*3}                   # Без изменения — не суффикс.
echo ${stringZ%$(_abc)}               # abcABC123ABC
echo ${arrayZ[@]%abc}                 # Применяется к каждому элементу массива.
 
# Chet Ramey обещал исправить в последующих версиях Bash.
# echo ${sparseZ[@]%abc}              # В версии 2.05b — core dumps.
 
# -Это было бы здорово- Last-Subscript-Of (Последний-Индекс-Массива)
# echo ${#sparseZ[@]%*} # line 830: ${#sparseZ[@]%*}: bad substitution
 
echo
echo '- Наибольший суффикс -'
echo ${stringZ%%1*3}                  # Без изменения — не суффикс.
echo ${stringZ%%b*c}                  # a
echo ${arrayZ[@]%%b*c}                # a ABCABC 123123 ABCABC a
 
# Chet Ramey обещал исправить в последующих версиях Bash.
# echo ${sparseZ[@]%%b*c}             # В версии 2.05b — core dumps.
 
echo
echo '- - Замена подстроки - -'
echo '- - Подстрока может находиться в любом месте в строке. - -'
echo '- - Первый описатель задает шаблон поиска - -'
echo '- - Шаблон может быть строкой или результатом работы функции. - -'
echo '- - Второй описатель может быть строкой или результатом работы функции. - -'
echo '- - Второй описатель может быть опущен. В результате получится:'
echo ' Заменить-Ничем (Удалить) - -'
echo
 
# Функция, результатом работы которой является обычная строка
_123() {
 echo -n '123'
}
 
echo '- Замена первого вхождения -'
echo ${stringZ/$(_123)/999}            # Подстрока 123 заменена на 999).
echo ${stringZ/ABC/xyz}                # xyzABC123ABCabc
echo ${arrayZ[@]/ABC/xyz}              # Применяется ко всем элементам массива.
echo ${sparseZ[@]/ABC/xyz}             # Работает так как и ожидается.
 
echo
echo '- Удаление первого вхождения -'
echo ${stringZ/$(_123)/}
echo ${stringZ/ABC/}
echo ${arrayZ[@]/ABC/}
echo ${sparseZ[@]/ABC/}
 
# Замещающий элемент необязательно должен быть строкой,
# допускается употреблять результат функции.
# Это применимо ко всем формам замены.
 
echo
echo '- Замена первого вхождения результатом работы функции -'
echo ${stringZ/$(_123)/$(_simple)}     # Работает так как и ожидается.
echo ${arrayZ[@]/ca/$(_simple)}        # Применяется ко всем элементам массива.
echo ${sparseZ[@]/ca/$(_simple)}       # Работает так как и ожидается.
 
echo
echo '- Замена всех вхождений -'
echo ${stringZ//[b2]/X}                # все символы b и 2 заменяются символом X
echo ${stringZ//abc/xyz}               # xyzABC123ABCxyz
echo ${arrayZ[@]//abc/xyz}             # Применяется ко всем элементам массива.
echo ${sparseZ[@]//abc/xyz}            # Работает так как и ожидается.
 
echo
echo '- Удаление всех вхождений -'
echo ${stringZ//[b2]/}
echo ${stringZ//abc/}
echo ${arrayZ[@]//abc/}
echo ${sparseZ[@]//abc/}
 
echo
echo '- - Замена префикса - -'
echo '- - Шаблон должен совпадать с первым символом строки. - -'
echo
 
echo '- - Замена префикса - -'
echo ${stringZ/#[b2]/X}                 # Без изменения — не префикс.
echo ${stringZ/#$(_abc)/XYZ}            # XYZABC123ABCabc
echo ${arrayZ[@]/#abc/XYZ}              # Применяется ко всем элементам массива.
echo ${sparseZ[@]/#abc/XYZ}             # Работает так как и ожидается.
 
echo
echo '- Удаление префикса -'
echo ${stringZ/#[b2]/}
echo ${stringZ/#$(_abc)/}
echo ${arrayZ[@]/#abc/}
echo ${sparseZ[@]/#abc/}
 
echo
echo '- - Замена суффикса - -'
echo '- - Шаблон должен совпадать с последним символом строки. - -'
echo
 
echo '- - Замена суффикса - -'
echo ${stringZ/%[b2]/X}                 # Без изменения — не суффикс.
echo ${stringZ/%$(_abc)/XYZ}            # abcABC123ABCXYZ
echo ${arrayZ[@]/%abc/XYZ}              # Применяется ко всем элементам массива.
echo ${sparseZ[@]/%abc/XYZ}             # Работает так как и ожидается.
 
echo
echo '- Удаление суффикса -'
echo ${stringZ/%[b2]/}
echo ${stringZ/%$(_abc)/}
echo ${arrayZ[@]/%abc/}
echo ${sparseZ[@]/%abc/}
 
echo
echo '- - Специальный случай -- пустой шаблон поиска - -'
echo
 
echo '- Префикс -'
# пустой шаблон означает простую вставку в начало строки
echo ${stringZ/#/NEW}                   # NEWabcABC123ABCabc
echo ${arrayZ[@]/#/NEW}                 # Применяется ко всем элементам массива.
echo ${sparseZ[@]/#/NEW}                # И к неинициализированным элементам тоже
 
echo
echo '- Суффикс -'
# пустой шаблон означает простое добавление в конец строки
echo ${stringZ/%/NEW}                   # abcABC123ABCabcNEW
echo ${arrayZ[@]/%/NEW}                 # Применяется ко всем элементам массива.
echo ${sparseZ[@]/%/NEW}                # И к неинициализированным элементам тоже
 
echo
echo '- - Специальный случай оператора For-Each - -'
echo '- - - - This is a nice-to-have dream - - - -'
echo
 
_GenFunc() {
 echo -n ${0}                            # Только как иллюстрация.
 # Фактически здесь может стоять любое другое выражение.
}
 
# Все вхождения, совпадающие с шаблоном "*"
# В настоящее время, шаблон //*/ не совпадает с пустыми
# и неинициализированными элементами.
# В то время как шаблоны /#/ и /%/ совпадают с пустыми и не совпадают
# с неинициализированными элементами.
 
echo ${sparseZ[@]//*/$(_GenFunc)}
 
exit 0

Приложение B. Справочная информация

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

Таблица B-1. Переменные специального назначения

Переменная Описание
$0 Имя файла сценария
$1 Позиционный параметр #1 (аргумент сценария или функции)
$2 - $9 Позиционные параметры #2 - #9
${10} Позиционный параметр #10
$# Количество позиционных параметров
"$*" Все позиционные параметры(как одно слово. Необходимо

заключать в кавычки, в противном случае будет работать как "$@")

"$@" Все позиционные параметры(в виде отдельных строк)
${#*} Количество позиционных параметров,

переданных из командной строки

${#@} Количество позиционных параметров,

переданных из командной строки

$? Возвращаемое значение
(Код возврата последней команды)
$$ Идентификатор процесса — Process ID (PID) сценария
$- Флаги, переданные сценарию(командой set)
$_ Последний аргумент предыдущей команды
$! Идентификатор последнего фонового процесса (PID)'

Таблица B-2. Операции проверки: Двухместные операции

Оператор Значение ---- Оператор Значение
Арифметическое сравнение
Сравнение строк
-eq Равно = Равно
== Равно
-ne Не равно != Не равно
-lt Меньше \< Меньше(в кодах ASCII)*
-le Меньше или равно
-gt Больше \> Больше(в кодах ASCII)*
-ge Больше или равно
-z Пустая строка
-n Не пустая строка
Арифметическое сравнение
В двойных круглых скобках((... ))
> Больше
>= Больше или равно
< Меньше
<= Меньше или равно
*При использовании двойных квадратных скобок [[...]], необходимо использовать экранирующий символ — \.

Таблица B-3. Операции проверки: Файлы

Оператор Что проверяется ---- Оператор Что проверяется
-e Файл существует -s Файл не нулевой длины
-f Обычный файл
-d Файл является каталогом -r Файл доступен для чтения
-h Файл является символической ссылкой -w Файл доступен для записи
-L Файл является символической ссылкой -x Файл доступен для исполнения
-b Файл блочного устройства
-c Файл символьного устройства -g Установлен флаг sgid
-p Файл является каналом (pipe) -u Установлен флаг suid
-S Файл является сокетом (socket) -k Установлен бит "sticky"
-t Файл связан с терминальным устройством
-N Файл был модифицирован с момента последнего чтения F1 -nt F2 Файл F1 более новый, чем F2*
-O Вы являетесь владельцем файла F1 -ot F2 Файл F1 более старый, чем F2*
-G Вы принадлежите к той же группе, что и файл F1 -ef F2 Файлы F1 и F2 являются жёсткими

ссылками на один тот же файл

! "NOT" (логическое отрицание(инверсия) результатов всех вышеприведенных проверок)
*Двухместный оператор (требует наличия двух операндов).

Таблица B-4. Подстановка параметров и экспансия

Выражение Описание
${var} Значение переменной var, то же, что и $var
${var-DEFAULT} Если переменная var не инициализирована, то результатом

вычисления выражения является $DEFAULT*

${var:-DEFAULT} Если переменная var не инициализирована или пуста, то

результатом вычисления выражения является $DEFAULT*

${var=DEFAULT} Если переменная var не инициализирована, то результатом

вычисления выражения является $DEFAULT*

${var:=DEFAULT} Если переменная var не инициализирована, то результатом

вычисления выражения является $DEFAULT*

${var+OTHER} Если переменная var инициализирована, то результатом вычисления

выражения является $OTHER, иначе — пустая строка

${var:+OTHER} Если переменная var инициализирована set, то результатом вычисления

выражения является $OTHER, иначе — пустая строка

${var?ERR_MSG} Если переменная var не инициализирована, то выводится $ERR_MSG*
${var:?ERR_MSG} Если переменная var не инициализирована, то выводится $ERR_MSG*
${!varprefix*} Соответствует всем ранее объявленным переменным, чьи имена

начинаются с varprefix

${!varprefix@} Соответствует всем ранее объявленным переменным, чьи имена

начинаются с varprefix

*Само собой разумеется, если переменная var инициализирована, то результатом выражения будет $var.

Таблица B-5. Операции со строками

Выражение Описание
${string:position} Извлечение подстроки из строки $string,

начиная с позиции $position

${#string} Длина строки $string
${string:position:length} Извлечение $length символов из строки

$string, начиная с позиции $position

${string#substring} Поиск кратчайшего совпадения по шаблону $substring,

в строке $string, поиск ведётся с начала строки

${string##substring} Поиск самого длинного совпадения по шаблону

$substring, в строке $string, поиск ведется с начала строки

${string%substring} Поиск кратчайшего совпадения по шаблону $substring,

в строке $string, поиск ведётся с конца строки

${string%%substring} Поиск самого длинного совпадения по шаблону

$substring, в строке $string, поиск ведётся с конца строки

${string/substring/replacement} Замена первой, найденной по

шаблону $substring, подстроки на подстроку $replacement

${string//substring/replacement} Замена всех, найденных по

шаблону $substring, подстрок на подстроку $replacement

${string/#substring/replacement} Если в строке $string найдено соответствие шаблону $substring и найденная

подстрока начинает строку $string, то она заменяется подстрокой $replacement

Если в строке $string найдено соответствие шаблону $substring и найденная подстрока

заканчивает строку $string, то она заменяется подстрокой $replacement

expr match "$string" '$substring' Количество совпадений с шаблоном $substring*

в строке $string, поиск начинается с начала строки

expr "$string" : '$substring' Количество совпадений с шаблоном $substring*

в строке $string, поиск начинается с начала строки

expr index "$string" $substring Позиция(номер символа), первого найденного

совпадения с шаблоном $substring, в строке $string

expr substr $string $position $length Извлечение $length символов из строки $string,

начиная с позиции $position

expr match "$string" '\($substring\)' Извлечение подстроки с начала строки $string,

совпадающей с шаблоном $substring*

expr "$string" : '\($substring\)' Извлечение подстроки с начала строки $string,

совпадающей с шаблоном $substring*

expr match "$string" '.*\($substring\)' Извлечение подстроки с конца строки $string,

совпадающей с шаблоном $substring*

expr "$string" : '.*\($substring\)' Извлечение подстроки с конца строки $string,

совпадающей с шаблоном $substring*

* Где $substring — регулярное выражение.

Таблица B-6. Прочие конструкции

Выражение Описание
Квадратные скобки
if [ CONDITION ] Проверка условия
if [[ CONDITION ]] Расширенный синтаксис операции проверки условия
Array[1]=element1 Инициализация массива
[a-z] Диапазон символов в регулярных выражениях
Фигурные скобки
${variable} Подстановка параметра
${!variable} Косвенная ссылка на переменную
{ command1; command2 } Блок кода
{string1,string2,string3,...} Подстановка
Круглые скобки
( command1; command2 ) Группа команд, исполняемая в подоболочке(subshell)
Array=(element1 element2element3) Инициализация массива
result=$(COMMAND) Команда исполняется в подоболочке, результат записывается в переменную
>(COMMAND) Подстановка процесса
<(COMMAND) Подстановка процесса
Двойные круглые скобки
(( var = 78 )) Целочисленная арифметика
var=$(( 20 + 5 )) Арифметическое выражение, результат записывается в переменную
Кавычки
"$variable" "Мягкие", или "нестрогие" кавычки
'string' "Строгие", или "полные" кавычки
Обратные кавычки
result=`COMMAND` Команда исполняется в подоболочке, результат записывается в переменную

Таблица B-7. Выражения, используемые в инструкции if

Первичное выражение Значение Первичное выражение Значение
[ -a FILE ] Истина, если FILE существует. [ -b FILE ] Истина, если FILE существует и является блочным файлом.
[ -c FILE ] Истина, если FILE существует и является строковым файлом. [ -d FILE ] Истина, если FILE существует и является директорией.
[ -e FILE ] Истина, если FILE существует. [ -f FILE ] Истина, если FILE существует и является обычным файлом.
[ -g FILE ] Истина, если FILE существует и у него установлен бит SGID. [ -h FILE ] Истина, если FILE существует и является символической ссылкой.
[ -k FILE ] Истина, если FILE существует и у него установлен промежуточный бит округления. [ -p FILE ] Истина, если FILE существует и является именованным конвейером (FIFO).
[ -r FILE ] Истина, если FILE существует и доступен для чтения. [ -s FILE ] Истина, если FILE существует и его размер больше нуля.
[ -t FD ] Истина, если дескриптор файла FD открыт и ссылается на терминал. [ -u FILE ] Истина, если FILE существует и у него
установлен бит SUID (установлен идентификатор пользователя ID)
[ -w FILE ] Истина, если FILE существует и доступен для записи. [ -x FILE ] Истина, если FILE существует и является исполняемым.
[ -O FILE ] Истина, если FILE существует и его владельцем является действующий пользователь. [ -G FILE ] Истина, если FILE существует и его владелец принадлежит к действующей группе.
[ -L FILE ] Истина, если FILE существует и является символической ссылкой. [ -N FILE ] Истина, если FILE существует и был модифицирован после последнего чтения.
[ -S FILE ] Истина, если FILE существует и является сокетом. [ FILE1 -nt FILE2 ] Истина, если FILE1 был изменен раньше, чем FILE2,
либо если FILE1 существует, а FILE2 не существует.
[ FILE1 -ot FILE2 ] Истина, если FILE1 изменен позже, чем FILE2, либо если FILE2 существует, а FILE1 не существует. [ FILE1 -ef FILE2 ] Истина, если FILE1 и FILE2 ссылаются на одно и то же устройство или на одинаковые номера inode.
[ -o OPTIONNAME ] Истина, если установлен параметр командной оболочки "OPTIONNAME". [ -z "STRING" ] Истина, если длина строки "STRING" равна нулю.
[ -n "STRING" ]
или
[ "STRING" ]
Истина, если длина строки "STRING" ненулевая. [ "STRING1" == "STRING2" ] Истина, если строки равны. Для более строгого
соответствия POSIX вместо "==" можно использовать "=".
[ "STRING1" != "STRING2" ] Истина, если строки не равны [ "STRING1" < "STRING2" ] Истина, если в текущей локали при лексикографической
сортировке "STRING1" оказывается перед "STRING2".
[ "STRING1" > "STRING2" ] Истина, если в текущей локали при лексикографической
сортировке "STRING1" оказывается после "STRING2".
[ ARG1 OP ARG2 ] "OP" — один из операторов -eq, -ne, -lt, -le, -gt или -ge(см. Таблица B-2. Операции проверки: Двухместные операции)."ARG1" и "ARG2" являются целыми числами.
[ ! EXPR ] Истина, если EXPR ложно. [ ( EXPR ) ] Возвращает значение EXPR. Используется для
изменения нормального предшествования операторов.
[ EXPR1 -a EXPR2 ] Истина, если оба EXPR1 и EXPR2 являются истинными. [ EXPR1 -o EXPR2 ] Истина, если либо EXPR1, либо EXPR2 истинно.

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

В этом приложении содержится очень краткое описание приемов работы с утилитами обработки текста sed и awk. Здесь будут рассмотрены лишь несколько базовых команд, которых, в принципе, будет достаточно, чтобы научиться понимать простейшие конструкции sed и awk внутри сценариев на языке командной оболочки.
sed: неинтерактивный редактор текстовых файлов
awk: язык обработки шаблонов с C-подобным синтаксисом
При всех своих различиях, эти две утилиты обладают похожим синтаксисом, они обе умеют работать с регулярными выражениями, обе, по-умолчанию, читают данные с устройства stdin и обе выводят результат обработки на устройство stdout. Обе являются утилитами Unix-систем, и прекрасно могут взаимодействовать между собой. Вывод от одной может быть перенаправлен, по конвейеру, на вход другой. Их комбинирование придаёт сценариям, на языке командной оболочки, мощь и гибкость языка Perl.
Одно важное отличие состоит в том, что в случае с sed, сценарий легко может передавать дополнительные аргументы этой утилите, в то время, как в случае с awk это более сложная задача.

C 1. Sed

Sed — это неинтерактивный строчный редактор. Он принимает текст либо с устройства stdin, либо из текстового файла, выполняет некоторые операции над строками и затем выводит результат на устройство stdout или в файл. Как правило, в сценариях, sed используется в конвейерной обработке данных, совместно с другими командами и утилитами. Sed определяет, по заданному адресному пространству, над какими строками следует выполнить операции. Адресное пространство строк задаётся либо их порядковыми номерами, либо шаблоном. Например, команда 3d заставит sed удалить третью строку, а команда /windows/d означает, что все строки, содержащие "windows", должны быть удалены. Из всего разнообразия операций, мы остановимся на трёх, используемых наиболее часто. Это p — печать (на stdout), d — удаление и s — замена.

Таблица C-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 Операция выполняется над всеми найденными соответствиями

внутри каждой из заданных строк


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

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

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

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

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

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

Таблица C-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
}

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

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

.

Ссылки на дополнительные сведения о sed, вы найдёте в разделе Библиография данной книги.

C 2. Awk

Awk — это полноценный язык обработки текстовой информации с синтаксисом, напоминающим синтаксис языка C. Он обладает довольно широким набором возможностей, однако, мы рассмотрим лишь некоторые из них — наиболее употребимые в сценариях командной оболочки. Awk "разбивает" каждую строку на отдельные поля. По-умолчанию, поля — это последовательности символов, отделённые друг от друга пробелами, однако имеется возможность назначения других символов, в качестве разделителя полей. Awk анализирует и обрабатывает каждое поле в отдельности. Это делает его идеальным инструментом для работы со структурированными текстовыми файлами — особенно с таблицами.
Внутри сценариев командной оболочки, код awk, заключается в "строгие" (одиночные) кавычки и фигурные скобки.

awk '{print $3}' $filename
# Выводит содержимое 3-го поля из файла $filename на устройство stdout.
 
awk '{print $1 $5 $6}' $filename
# Выводит содержимое 1-го, 5-го и 6-го полей из файла $filename.

Только что, мы рассмотрели действие команды print. Ещё, на чём мы остановимся — это переменные. Awk работает с переменными подобно сценариям командной оболочки, но более гибко.

{ total += ${column_number} }

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

END { print total }

Команде END, соответствует команда BEGIN, которая открывает блок кода, отрабатывающий перед началом обработки входных данных. Следующий пример демонстрирует применение awk для разбора текста в сценариях командной оболочки.

Пример: C-1. Подсчёт количества символов.

#! /bin/sh
# letter-count.sh: Counting letter occurrences in a text file.
#
# Автор: nyal (nyal@voila.fr).
# Используется с разрешения автора сценария.
# Комментарии добавлены автором документа.
 
INIT_TAB_AWK=""
 
count_case=0
FILE_PARSE=$1
E_PARAMERR=65
 
usage()
{
 echo "Порядок использования: letter-count.sh имя_файла символы" 2>&1
 # Например: ./letter-count.sh filename.txt a b c
 exit $E_PARAMERR # Вызов сценария без аргументов.
}
 
if [ ! -f "$1" ] ; then
 echo "Файл $1 не найден." 2>&1
 usage                   # Вывод сообщения и выход.
fi
 
if [ -z "$2" ] ; then
 echo "$2: Не заданы символы для поиска." 2>&1
 usage
fi
 
shift                    # Символы заданы.
for letter in `echo $@`  # Для каждого из них . . .
do
 INIT_TAB_AWK="$INIT_TAB_AWK tab_search[${count_case}] = \"$letter\"; final_tab[${count_case}] = 0;"
 # Передаётся как параметр в сценарий awk ниже.
 count_case=`expr $count_case + 1`
done
 
# DEBUG:
# echo $INIT_TAB_AWK;
cat $FILE_PARSE |
# Передать заданный файл по конвейеру в сценарий awk.
 
# --------------------------------------------------------------------------------
# От переводчика:
# В оригинальном тексте сценария стоит следующая строка:
#awk -v tab_search=0 -v final_tab=0 -v tab=0 -v nb_letter=0 -v chara=0 -v chara2=0 \
#
# с моим gawk 3.1.3 сценарий делается неработоспособным
# поэтому я взял на себя смелость несколько подправить эту строку,
# в результате она получилась такой:
awk -v nb_letter=0 -v chara=0 -v chara2=0 \
"BEGIN { $INIT_TAB_AWK } \
{ split(\$0, tab, \"\"); \
for (chara in tab) \
{ for (chara2 in tab_search) \
{ if (tab_search[chara2] == tab[chara]) { final_tab[chara2]++ } } } } \
END { for (chara in final_tab) \
{ print tab_search[chara] \" => \" final_tab[chara] } }"
# --------------------------------------------------------------------------------
# Ничего сложного...
# Циклы for, проверка условия if, и пара специфических функций.
 
exit $?

Это всё, что я хотел рассказать об awk. Дополнительные ссылки на информацию об awk, вы найдёте в разделе Библиография.

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

Таблица D-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 имеют предопределённое значение, поэтому вам следует избегать употребления этих кодов для своих нужд. Завершение сценария с кодом возврата exit 127, может привести в замешательство при поиске ошибок в сценарии (действительно ли он означает ошибку "команда не найдена"? Или это предусмотренный программистом код завершения?). В большинстве случаев, программисты вставляют exit 1, в качестве реакции на ошибку. Так как код завершения 1 подразумевает целый "букет" ошибок, то в данном случае трудно говорить о какой-либо двусмысленности, хотя и об информативности — тоже.
Не раз предпринимались попытки систематизировать коды завершения (см. /usr/include/sysexits.h), но эта систематизация предназначена для программистов, пишущих на языках C и C++. Автор документа предлагает ограничить коды завершения, определяемые пользователем, диапазоном 64-113 (и, само собой разумеется — 0, для обозначения успешного завершения), в соответствии со стандартом C/C++. Это сделало бы поиск ошибок более простым. Все сценарии, прилагаемые к данному документу, приведены в соответствие с этим стандартом, за исключением случаев, когда существуют отменяющие обстоятельства.
!Обращение к переменной $?, из командной строки, после завершения работы сценария, даёт результат, в соответствии с таблицей, приведенной выше, но только для Bash или sh. Под управлением csh или tcsh значения могут в некоторых случаях отличаться.

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

Написано 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

Приложение F. Системные каталоги

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

  • /bin
Каталог с программами. В этом каталоге размещаются основные системные программы и утилиты (например bash).
  • /usr/bin
Дополнительные системные каталоги.
  • /usr/local/bin
Программы разного рода.
  • /sbin
Superuser binaries — программы суперпользователя. Основные программы и утилиты административного назначения (например fsck).
  • /usr/sbin
Дополнительные программы суперпользователя.
  • /etc
Et cetera. Конфигурационные системные скрипты.
  • /etc/rc.d
Сценарии начальной загрузки системы, для дистрибутива Red Hat и его клонов.
  • /usr/share/doc
Документация к установленным пакетам.
  • /tmp
Временный каталог системы.
  • /var/log
Каталог для хранения системных журналов (логов).
  • /var/spool/mail
Буфер для электронной почты.

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

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

#!/bin/bash
# localized.sh
# Автор Stephane Chazelas, дополнил Bruno Haible
 
. gettext.sh
 
E_CDERROR=65
 
error()
{
 printf "$@" >&2
 exit $E_CDERROR
}
 
cd $var || error "`eval_gettext \"Can't cd to \$var.\"`"
read -p "`gettext \"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.
!Bruno Haible отмечает, что:

Начиная с версии gettext-0.12.2, вместо bash --dump-po-strings localized.sh, рекомендуется использовать xgettext -o - localized.sh, потому что xgettext...
1. понимает команды gettext и eval_gettext (тогда как bash --dump-po-strings только свой синтаксис в виде $"..." )
2. может извлекать комментарии программиста, предназначенные для переводчика.

Такой код не привязан к определенной версии Bash и может быть исполнен в Bash 1.x или других реализациях /bin/sh.
Теперь построим файл 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

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

! В старых версиях 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, и дополнено Bruno Haible, ведущим разработчиком GNU gettext.

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

Командная оболочка 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. $HISTFILESIZE
  5. $HISTSIZE
  6. !!
  7. !$
  8. !#
  9. !N
  10. !-N
  11. !STRING
  12. !?STRING?
  13. ^STRING^string^

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

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

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

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

Файл ~/.bashrc определяет поведение командной оболочки. Внимательное изучение этого примера поможет вам значительно продвинуться в понимании Bash. Emmanuel Rouat представил следующий, очень сложный, файл .bashrc, написанный для операционной системы Linux. Предложения и замечания приветствуются.
Внимательно изучите этот файл. Отдельные участки этого файла вы свободно можете использовать в своём собственном .bashrc или, даже в своих сценариях!

Пример I-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 ()
_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
# Конец:

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

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

Таблица J-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 Cдвиг списка аргументов
командной строки влево
SGN -lt или -gt Знак (целого числа)
ERRORLEVEL $? Код завершения
CON stdin "Консоль" (stdin)
PRN /dev/lp0 Устройство принтера
LPT1 /dev/lp0 Устройство принтера
COM1 /dev/ttyS0 Первый последовательный порт

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

Таблица J-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 Скопировать файл(расширенная команда)

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

Пример: Пример J-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

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

Пример: Пример J-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. Определённо, его изобретательность будет вам полезна, при создании ваших сценариев.

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

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

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

#!/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 символов, и на место гласных вставлять "живые особи". Подсказка: не забудьте преобразовать пробелы в символы подчеркивания.

K2. Создание сценариев

Напишите сценарии для выполнения повседневных задач.

Простые задания
Содержимое домашнего каталога

Выполните рекурсивный обход домашнего каталога и сохраните информацию в файл. Сожмите файл. Попросите пользователя вставить дискету и нажать клавишу ENTER. Запишите сжатый файл на дискету.

Замена цикла for циклами while и until

Замените циклы for в Пример: Простой цикл for (из главы Циклы и ветвления) на while. Подсказка: запишите данные в массив и пройдите в цикле по элементам массива.
Выполнив эту "тяжёлую работу", замените циклы, в этом примере, на циклы until.

Изменение межстрочного интервала в текстовом файле

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

Вывод "задом-на-перёд"

Напишите сценарий, который будет выводить себя на stdout, но в обратном порядке.

Автоматическое разархивирование

Для каждого файла, из заданного списка, сценарий должен определить тип архиватора, которым был создан тот или иной файл (с помощью утилиты file). Затем сценарий должен выполнить соответствующую команду разархивации (gunzip, bunzip2, unzip, uncompress или что-то иное). Если файл не является архивом, то сценарий должен оповестить пользователя об этом и ничего не делать с этим файлом.

Уникальный идентификатор системы

Сценарий должен сгенерировать "уникальный" 6-ти разрядный шестнадцатиричный идентификатор системы. Не пользуйтесь дефектной утилитой hostid. Подсказка: md5sum /etc/passwd', затем отберите первые 6 цифр.

Резервное копирование

Сценарий должен создать архив (*.tar.gz) всех файлов в домашнем каталоге пользователя(/home/user-name), которые изменялись в течение последних 24 часов. Подсказка: воспользуйтесь утилитой find.

Простые числа

Сценарий должен вывести (на stdout) все простые числа, в диапазоне от 60000 до 63000. Вывод должен быть отформатирован по столбцам (подсказка: воспользуйтесь командой printf).

Лототрон

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


Задания повышенной сложности
Управление дисковым пространством

Сценарий должен отыскать в домашнем каталоге пользователя /home/username файлы, имеющие размер больше 100K. Каждый раз предоставляя пользователю возможность удалить или сжать этот файл, затем переходить к поиску следующего файла.

Безопасное удаление

Напишите сценарий "безопасного" удаления файлов — srm.sh. Файлы, с именами, передаваемыми этому сценарию, не должны удаляться, вместо этого, файлы следует сжать утилитой gzip, если они ещё не сжаты (не забывайте про утилиту file), и переместить в каталог /home/username/trash. При старте, сценарий должен удалять из каталога "trash" файлы, которые были созданы более 48 часов тому назад.

Размен монет

Как более рационально собрать сумму в $1.68, используя только монеты, с номиналом не выше 25c? Это будет шесть 25-ти центовых монет, одна десятицентовая, одна пятицентовая и три монеты достоинством в 1 цент.
Учитывая возможность произвольного ввода суммы в долларах и центах ($*.??), найдите такую комбинацию, которая требовала бы наименьшее число монет. Если вы проживаете не в США, то можете использовать свою денежную единицу и номиналы монет. Подсказка: взгляните на Пример: Преобразование чисел в римскую форму записи.

Корни квадратного уравнения

Напишите сценарий, который находил бы корни "квадратного " уравнения, вида: Ax^2+Bx+C=0. Сценарий должен получать коэффициенты уравнения A, B и C, как аргументы командной строки, и находить корни, с точностью до четвёртого знака после запятой.
Подсказка: воспользуйтесь bc, для нахождения решения по хорошо известной формуле: x = ( -B+/- sqrt( B^2 - 4AC ) ) / 2A.

Сумма чисел

Найдите сумму всех пятизначных чисел (в диапазоне 10000-99999), которые содержат точно две цифры из следующего набора: {4, 5, 6}.
Примеры чисел, удовлетворяющих данному условию: 42057, 74638 и 89515.

Счастливый билет

"Счастливым" считается такой билет, в котором последовательное сложение цифр номера даёт число 7. Например, 62431 — номер "счастливого" билета (6+2+4+3+1=16, 1+6 = 7). Найдите все "счастливые" номера, располагающиеся в диапазоне 1000-10000.

Синтаксический анализ

Проанализируйте файл /etc/passwd и выведите его содержимое в табличном виде.

Контроль входов в систему

Проанализируйте файл /var/log/messages и выведите в отформатированном виде сведения по каждому пользователю — когда он входил в систему. Возможно, что для работы сценарию потребуются права root. (Подсказка: Ищите строки, содержащие текст "LOGIN")

Просмотр файла с данными

Некоторые базы данных и электронные таблицы используют формат CSV (comma-separated values — значения, разделённые запятыми), для хранения данных в файлах. Зачастую, эти файлы должны анализироваться другими приложениями.
Пусть файл содержит следующие данные:
Jones,Bill,235 S. Williams St.,Denver,CO,80221,(303) 244-7989
Smith,Tom,404 Polk Ave.,Los Angeles,CA,90003,(213) 879-5612...
Прочитайте данные и выведите их на stdout в виде колонок с заголовками.

Выравнивание

Текст вводится с устройства stdin или из файла. Его необходимо вывести на stdout, с выравниванием по ширине, используя задаваемую пользователем ширину строк.

Список рассылки

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

Генерация паролей

Сгенерируйте псевдослучайные 8-ми символьные пароли, используя символы из диапазона [0-9], [A-Z], [a-z]. Каждый пароль должен содержать не менее 2-х цифр.


Сложные задания
Проверка паролей

Напишите сценарий проверки простоты раскрытия заданного пароля.
Исследуемый пароль должен передаваться сценарию как аргумент командной строки. Пароль должен отвечать следующим минимальным требованиям:
  • Минимальная длина — 8 символов
  • Должен содержать хотя бы одну цифру
  • Должен содержать хотя бы один из следующих символов: @, #, $, %, &, *, +, -, =
Необязательно:
  • Проверка возможности подбора по словарю каждой последовательной четвёрки алфавитных символов. Это исключит вхождение "словарных словwords" в пароль.
  • Проверьте с помощью этого сценария все пароли в вашей системе, которые могут располагаться в /etc/passwd (или в другом месте).
Для создания такого сценария вам потребуется умение работать с регулярными выражениями.

Регистрация обращений к файлам

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

Мониторинг процессов

Напишите сценарий, который производит непрерывный мониторинг запускаемых процессов и отслеживает количество дочерних процессов для каждого "родителя". Как только процесс порождает более 5 дочерних процессов, сценарий должен послать уведомление системному администратору (или root-у) с соответствующей информацией. В письме необходимо указать время, PID "родителя", PID каждого "потомка" и пр.. Сценарий должен писать отчет в системный журнал один раз в 10 минут.

Удаление комментариев

Удалите все комментарии из сценария, имя которого задается с командной строки. При этом, строка "#! /bin/bash" не должна удаляться.

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

Удалите все теги HTML из заданного HTML файла, затем переформатируйте его так, чтобы строки не были короче 60 и длиннее 75 символов. Предусмотрите оформление параграфов. Преобразуйте таблицы HTML в их приблизительный текстовый эквивалент.

Преобразование XML файлов

Преобразуйте файл из формата XML в формат HTML и в простой текстовый файл.

Борьба со спамом

Напишите сценарий, который анализировал бы входящие почтовые сообщения на принадлежность к спаму и отыскивал бы в DNS имена узлов сети, по IP адресам из заголовка письма. Сценарий должен отправлять найденные спамерские сообщения ответственным за спам-провайдерам (ISP). Естественно, вы должны отфильтровать свой собственный IP-адрес, чтобы не случилось так, что вы жалуетесь на самого себя.
По мере необходимости, используйте соответствующие команды для работы с сетью.

Азбука Морзе

Преобразуйте текстовый файл в код Морзе. Символы из файла должны быть представлены в виде, соответствующих им, кодов Морзе, состоящих из точек и тире, и разделённых пробелами. Например, "script" ===> "... _._. ._. .. .__. _".

Шестнадцатиричный дамп

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

Эмуляция сдвигового регистра

Используя Пример: Эмуляция структуры "СТЕК" ("первый вошёл — последний вышел"), как образец, напишите сценарий, который эмулировал бы 64-х битный сдвиговый регистр в виде массива. Реализуйте функции загрузки значения в регистр, сдвиг влево и сдвиг вправо. В заключение, напишите функцию, которая интерпретировала бы содержимое "регистра" как восемь 8-ми битных символов ASCII.

Детерминант (определитель)

Найдите детерминант (определитель) матрицы 4x4.

Анаграммы

Сценарий должен запросить у пользователя 4-х символьное слово, и найти анаграммы для этого слова. Например, анаграммы к слову word: do or rod row word. Для поиска анаграмм можете использовать файл /usr/share/dict/linux.words.

Индекс сложности текста

"Индекс сложности текста" оценивает трудность понимания текста, как некое число, которое грубо соответствует количеству лет обучения в общеобразовательной школе. Например, индекс равный 8-ми говорит о том, что текст доступен для понимания человеку, окончившему 8-й класс общеобразовательной школы.
Вычисление индекса ведется по следующему алгоритму:
  1. Выберите кусок текста, длиной не менее 100 слов.
  2. Сосчитайте количество предложений.
  3. Найдите среднее число слов в предложении — СРЕДНЕЕ_ЧИСЛО_СЛОВ=ОБЩЕЕ_ЧИСЛО_СЛОВ/ЧИСЛО_ПРЕДЛОЖЕНИЙ
  4. Сосчитайте количество "трудных" слов — которые содержат не менее 3-х слогов. Разделите это число на общее количество слов, в результате вы получите пропорцию сложных слов — ПРОПОРЦИЯ_СЛОЖНЫХ_СЛОВ=ЧИСЛО_ДЛИННЫХ_СЛОВ/ОБЩЕЕ_ЧИСЛО_СЛОВ
  5. Индекс сложности текста рассчитывается как сумма двух этих чисел, умноженная на 0.4 и округлённая до ближайшего целого — ИНДЕКС_СЛОЖНОСТИ=int (0.4*( СРЕДНЕЕ_ЧИСЛО_СЛОВ +ПРОПОРЦИЯ_СЛОЖНЫХ_СЛОВ))
4-й пункт — самый сложный. Существуют различные алгоритмы подсчета слогов в словах. В данном же случае, вы можете ограничиться подсчетом сочетаний "гласный-согласный".
Строго говоря, при расчете индекса сложности не следует считать составные слова и имена собственные как "сложные" слова, но это слишком усложнит сценарий.

Вычисление числа пи по алгоритму "Игла Баффона"

В 18 веке, французский математик де Баффон (de Buffon) проделывал эксперимент, который заключался в бросании иглы, длиной "n", на деревянный пол, собранный из длинных и узких досок. Ширина всех досок пола одинакова и равна "d". Оказалось, что отношение общего числа бросков, к числу бросков, когда игла ложилась на щель, кратно числу пи.
Пользуясь Пример: Вычисление числа "пи", напишите сценарий, который использовал бы метод Монте Карло для эмуляции "Иглы Баффона". Для простоты примите длину иглы раной ширине досок, n = d.
Подсказка: особое значение здесь имеют переменные, которые будут вычисляться как расстояние от центра иглы до ближайшей щели и величина угла между иглой и щелью. Для выполнения расчётов можно воспользоваться утилитой bc.

Шифрование по алгоритму Playfair (Wheatstone)

Напишите сценарий, реализующий алгоритм шифрования Playfair (Wheatstone). В соответствии с этим алгоритмом, текст шифруется путем замены каждой 2-х символьной последовательности — "диграммы". Традиционно, в качестве ключа, используется матрица символов алфавита 5x5.
C O D E S
A B F G H
I K L M N
P Q R T U
V W X Y Z
Матрица содержит все символы алфавита, за исключением символа "J", который представляет символ "I". Первая строка матрицы — произвольно выбранное слово, в данном случае — "CODES", далее следуют символы алфавита, в порядке слева-направо, исключая те, которые входят в состав первой строки.
Шифрование производится по следующему алгоритму: для начала, текст сообщения разбивается на диграммы (группы по 2 символа). Если в диграмму попадают два одинаковых символа, то второй символ удаляется, и формируется новая диграмма. Если в последней группе остаётся один символ, то такая "неполная" диграмма дополняется "пустым" символом, обычно "X".
THIS IS A TOP SECRET MESSAGE
TH IS IS AT OP SE CR ET ME SA
Каждая диграмма может подпадать под одно из следующих определений:
  1. Оба символа находятся в одной строке ключа. Тогда, каждый из них заменяется символом,стоящим справа в той же строке. Если это последний символ строки ключа, то он заменяется первым символом в той же строке ключа.
  2. Оба символа находятся в одном столбце ключа. Тогда каждый из них заменяется на символ,стоящий ниже, в этом же столбце. Если это последний символ в столбце ключа, то он заменяется первым символом в том же столбце ключа.
  3. Символы диграммы стоят в вершинах прямоугольника. Тогда каждый из них заменяетсясимволом из соседнего, по горизонтали, угла. Диграмма "TH" соответствует 3-му определению.
G H
M N
T U (Прямоугольник с вершинами "T" и "H")
T --> U
H --> G
Диграмма "SE" соответствует 1-му определению.
C O D E S (Строка содержит оба символа "S" и "E")
S --> C (замена на первый символ в строке ключа)
E --> S
Дешифрация выполняется обратной процедурой, для случаев 1 и 2 — замена символом стоящим левее/выше. Для случая 3 — аналогично шифрации, т.е. заменяется символом из соседнего, по горизонтали, угла. Helen Fouche Gaines, в своей классической работе "Elementary Cryptoanalysis"(1939), приводит подробное описание алгоритма Playfair и методы его реализации.
Этот сценарий должен иметь три основных раздела:
  1. Генерация "ключевой матрицы", основывающейся на слове, которое вводит пользователь.
  2. Шифрование "плоского" текста сообщения.
  3. Дешифрование зашифрованного текста.
Широкое применение, в этом сценарии, найдут массивы и функции.

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

Приложение L. Хронология

Впервые этот документ вышел как HOWTO весной 2000 года. С тех пор он претерпел множество изменений и дополнений. Эта книга вряд ли появилась бы на свет без помощи всего Linux-сообщества, и особенно без помощи Linux Documentation Project.

Таблица L-1. Хронология

Редакция Дата Примечания
0.1 14 Июня 2000 Первая редакция.
0.2 30 Октября 2000 Исправление ошибок, сделаны добавления
в тексте, добавлены примеры сценариев.
0.3 12 Февраля 2001 Добавления
0.4 08 Июля 2001 Полная переработка книги.
0.5 03 Сентября 2001 Исправления ошибок, дополнительный
материал, реорганизация структуры книги.
1.0 14 Октября 2001 Стабильная редакция: исправления ошибок,
реорганизация структуры, дополнительный материал.
1.1 06 Января 2002 Исправления ошибок, дополнительный материал.
1.2 31 Марта 2002 Исправления ошибок, дополнительный материал.
1.3 02 Июня 2002 Редакция "Мандарин" (TANGERINE):
Исправления ошибок, дополнительный материал.
1.4 16 Июня 2002 Редакция "Манго" (MANGO):
Исправления ошибок, дополнительный материал.
1.5 13 Июля 2002 Редакция "Папайя" (PAPAYA):
Исправления ошибок, дополнительный материал.
1.6 29 Сентября 2002 Редакция "Гранат" (POMEGRANATE):
Исправления ошибок, дополнительный материал.
1.7 05 Января 2003 Редакция "Кокос" (COCONUT):
Исправления ошибок, дополнительный материал.
1.8 10 Мая 2003 Редакция "Плод хлебного дерева" (BREADFRUIT):
Исправления ошибок, дополнительный материал.
1.9 21 Июня 2003 Редакция "Хурма" (PERSIMMON):
Исправления ошибок, дополнительный материал.
2.0 24 Августа 2003 Редакция "Крыжовник" (GOOSEBERRY):
Дополнительный материал.
2.1 14 Сентября 2003 Редакция "Черника" (HUCKLEBERRY):
Исправления ошибок, дополнительный материал.
2.2 31 Октября 2003 Редакция "Клюква" (CRANBERRY):
Дополнительный материал.
2.3 03 Января 2004 Редакция "Земляника" (STRAWBERRY):
Исправления ошибок, дополнительный материал.
2.4 25 Января 2004 Редакция "Дыня" (MUSKMELON):
Исправления ошибок.
2.5 15 Февраля 2004 Редакция STARFRUIT:
Исправления ошибок, дополнительный материал.

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

Авторские права на книгу "Advanced Bash-Scripting Guide", принадлежат Менделю Куперу (MendelCooper). Автор так же заявляет о своих правах на все предыдущие версии этого документа. Этим соглашением признаются и охраняются авторские права третьих лиц, предоставивших свои материалы для данной книги. Этот документ может распространяться исключительно на условиях Open Publication License (версия 1.0 или выше), [1]. Соблюдение следующих пунктов лицензии обязательно:

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

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

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

Распространителям и издателям (включая электронные издания) запрещается наложение каких-либо ограничений, условий на данный документ, и все предыдущие его версии, без явного письменного разрешения автора. Начиная с этой версии, автор утверждает, что он НЕ связан какими-либо договорными отношениями, которые отменяли бы вышеизложенные высказывания.
Практически, вы можете свободно распространять неизмененную электронную версию этой книги. Вы должны получить явное разрешение автора на распространение изменённых версий книги или её производных. Цель этого ограничения состоит в том, чтобы сохранить художественную целостность данного документа и предотвратить появление побочных "ветвей".
Если вы выкладываете или распространяете этот документ, или какую-либо его предыдущую версию, на основе лицензии, иной, кроме перечисленных выше, тогда вы обязаны получить письменное разрешение автора книги, в противном случае ваши права на распространение документа являются незаконными.
Это очень либеральные условия и они не должны препятствовать законному распространению и использованию этой книги. Автор особенно поощряет использование этой книги в учебных целях. Права на коммерческое распространение книги могут быть получены у автора(thegrendel@theriver.com).
Автор произвёл этот документ в соответствии с буквой и духом LDP Manifesto.

Linux — это торговая марка, принадлежащая Линусу Торвальдсу (Linus Torvalds).

UNIX и Unix — это торговая марка, принадлежащая Open Group.
MS Windows — это торговая марка, принадлежащая Microsoft Corp.
Pentium — это торговая марка, принадлежащая Intel, Inc.
Scrabble — это торговая марка, принадлежащая Hasbro, Inc.
Все другие коммерческие торговые марки, упомянутые в данном документе, принадлежат их владельцам.

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