52595.fb2
Начав книгу с программирования в ОС Linux на языке С, теперь мы сделаем отступление и остановимся на написании программ в командной оболочке. Почему? ОС Linux не относится к системам, у которых интерфейс командной строки — запоздалое детище графического интерфейса. У систем UNIX, прообраза Linux, первоначально вообще не было графического интерфейса; все выполнялось из командной строки. Поэтому оболочка командной строки UNIX все время развивалась и превратилась в очень мощный инструмент. Эти свойства перекочевали и в Linux, и некоторые самые серьезные задачи вы можете выполнить наиболее легким способом именно из командной оболочки. Поскольку она так важна для ОС Linux и столь полезна для автоматизации простых задач, программирование средствами командной оболочки рассматривается прежде всего.
В этой главе мы познакомим вас с синтаксисом, структурами и командами, доступными при программировании в командной оболочке, как правило, используя интерактивные (основанные на экранах) примеры. Они помогут продемонстрировать функциональные возможности командной оболочки и собственные действия. Мы также бросим беглый взгляд на пару особенно полезных утилит режима командной строки, часто вызываемых из командной оболочки: grep и find. Рассматривая утилиту grep, мы познакомимся с основными положениями, касающимися регулярных выражений, которые появляются в утилитах ОС Linux и языках программирования, таких как Perl, Ruby и PHP. В конце главы вы узнаете, как писать настоящие сценарии, которые будут перепрограммироваться и расширяться на языке С на протяжении всей книги. В этой главе рассматриваются следующие темы:
□ что такое командная оболочка;
□ теоретические основы;
□ тонкости синтаксиса: переменные, условия и управление программой;
□ списки;
□ функции;
□ команды и их выполнение;
□ встроенные (here) документы;
□ отладка;
□ утилита grep и регулярные выражения;
□ утилита find.
Если вы столкнулись со сложным сценарием командной оболочки при администрировании системы, хотите смоделировать вашу последнюю грандиозную (но удивительно простую) идею или просто повысить производительность какой-либо итеративной задачи, эта глава вам будет полезна.
Одна из причин применения командной оболочки — возможность быстрого и простого программирования. Более того, командная оболочка всегда есть даже в самых упрощенных установках ОС Linux, поэтому благодаря простому моделированию вы сможете понять, работает ли ваша идея. Командная оболочка очень удобна и для небольших утилит, выполняющих относительно простую задачу, для которой производительность менее важна, чем простые настройка, сопровождение и переносимость. Вы можете применять оболочку для управления процессами, обеспечивая выполнение команд в заданном порядке, зависящем от успешного завершения каждого этапа выполнения.
Хотя внешне командная оболочка очень похожа на режим командной строки в ОС Windows, она гораздо мощнее и способна выполнять самостоятельно очень сложные программы. Вы можете не только выполнять команды и вызывать утилиты ОС Linux; но и разрабатывать их. Командная оболочка выполняет программы оболочки, часто называемые сценариями или скриптами, которые интерпретируются во время выполнения. Такой подход облегчает отладку, потому что вы легко можете выполнять программу построчно и не тратить время на перекомпиляцию. Но для задач, которым важно время выполнения или необходимо интенсивное использование процессора, командная оболочка оказывается неподходящей средой.
Вот мы и добрались до теоретических основ UNIX и, конечно, Linux. ОС UNIX основана на интенсивном многократном применении кода и зависит от него. Вы разработали маленькую простую утилиту, и пользователи применяют ее как одну из ссылок в строке, формирующей команду. Одно из наслаждений, доставляемых ОС Linux, — разнообразие имеющихся отличных средств. Примером может служить следующая команда:
$ ls -al | more
Эта команда применяет утилиты ls
и more
и передает вывод списка файлов для поэкранного отображения. Каждая утилита — это отдельный блок. Зачастую вы можете применять множество мелких утилит для создания больших и сложных комплексов программ.
Например, если вы хотите напечатать контрольную копию справочного руководства оболочки bash, примените следующую команду:
$ man bash | col -b | lpr
Более того, благодаря автоматической обработке типов файлов пользователям этих утилит обычно не нужно знать, на каком языке данные программы написаны. Если необходимо ускорить выполнение утилиты, как правило, ее сначала моделируют в командной оболочке и затем, когда работоспособность утилиты проверена, реализуют ее на языке С или С++, Perl, Python или каком-либо другом, обеспечивающем более быстрое выполнение. В противном случае, если в командной оболочке утилита действует адекватно, вы вполне можете оставить ее в покое.
Необходимость переделки сценария зависит от того, нуждается ли он в оптимизации, должен ли он быть переносимым, необходимо ли обеспечить легкую модификацию и не перерос ли он (как это обычно случается) первоначально поставленную задачу.
Для удовлетворения вашего любопытства в вашу ОС Linux уже загружены многочисленные примеры сценариев, включая инсталляторы пакетов, .xinitrc
и startx
, и сценарии в каталоге /etc/rc.d, предназначенные для настройки системы в процессе загрузки.
Прежде чем переходить к обсуждению того, как программа использует оболочку, давайте рассмотрим, как функционирует оболочка и какие оболочки есть в Linux-подобных системах. Командная оболочка — это программа, которая действует как интерфейс между вами и ОС Linux, позволяя вам вводить команды, которые должна выполнить операционная система. В этом смысле она похожа на командную строку в ОС Windows, но, как уже упоминалось, командные оболочки Linux гораздо мощнее. Например, ввод и вывод можно перенаправить с помощью символов <
и >
, передавать данные между двумя одновременно выполняющимися программами с помощью символа |
, а перехватывать вывод подпроцесса с помощью конструкции $(...)
. В ОС Linux вполне может сосуществовать несколько установленных командных оболочек, и разные пользователи могут выбрать ту, которая им больше нравится. На рис. 2.1 показано, как командная оболочка (на самом деле, две командные оболочки: bash и csh) и другие программы располагаются вокруг ядра Linux.
Рис. 2.1
Поскольку ОС Linux — модульная система, вы можете вставить и применять одну из множества различных стандартных командных оболочек, хотя большинство из них — потомки первоначальной оболочки Bourne. В Linux стандартная командная оболочка, всегда устанавливаемая как /bin/sh и входящая в комплект средств проекта GNU, называется bash (GNU Bourne-Again SHell). Именно ее мы будем применять, т. к. это отличная командная оболочка, всегда устанавливаемая в системах Linux, со свободно распространяемым программным кодом и переносимая почти на все варианты UNIX-систем. В данной главе используется оболочка bash версии 3, и в большинстве случаев применяются ее функциональные возможности, общие для всех командных оболочек, удовлетворяющих требованиям стандарта POSIX. Мы полагаем, что командная оболочка, установленная как /bin/sh и для вашей учетной записи, считается командной оболочкой по умолчанию. В большинстве дистрибутивов Linux программа /bin/sh, командная оболочка по умолчанию, — это ссылка на программу /bin/bash.
Вы можете определить используемую в вашей системе версию bash с помощью следующей команды:
$ /bin/bash --version
GNU bash, version 3.2.9(1)-release (i686-pc-linux-gnu)
Copyright (C) 2005 Free Software Foundation, Inc.
Для перехода на другую командную оболочку, если в вашей системе по умолчанию установлена не bash, просто выполните программу нужной вам командной оболочки (т.е. /bin/bash) для запуска новой оболочки и смены приглашения в командной строке. Если вы используете ОС UNIX, и командная оболочка bash не установлена, вы можете бесплатно загрузить ее с Web-сайта www.gnu.org. Исходный код обладает высокой степенью переносимости, и велика вероятность, что он откомпилируется в вашей версии UNIX прямо в готовую к использованию программу.
Когда создаются учетные записи пользователей ОС Linux, вы можете задать командную оболочку, которой они будут пользоваться, в момент создания учетной записи пользователя или позже, откорректировав ее параметры. На рис. 2.2 показан выбор командной оболочки для пользователя дистрибутива Fedora.
Рис. 2.2
Существует много других командных оболочек, распространяемых свободно или на коммерческой основе. В табл. 2.1 предлагается краткая сводка некоторых самых распространенных командных оболочек.
Таблица 2.1
Название командной оболочки | Краткие исторические сведения |
---|---|
sh (Bourne) | Первоначальная оболочка в ранних версиях ОС UNIX |
csh, tcsh, zsh | Командная оболочка C-shell (и ее производные), первоначально созданная Биллом Джойем (Bill Joy) для систем Berkeley UNIX. C-shell, возможно, третья по популярности командная оболочка после оболочек bash и Korn |
ksh, pdksh | Командная оболочка Korn и ее безлицензионный родственник. Написанная Дэвидом Корном (David Korn) эта оболочка применяется по умолчанию во многих коммерческих версиях UNIX |
bash | Основная командная оболочка ОС Linux из проекта GNU или Bourne Again SHell со свободно распространяемым программным кодом. Если в настоящий момент она не выполняется в вашей системе UNIX, вероятно, есть вариант оболочки, перенесенный на вашу систему. У bash много сходств с оболочкой Korn |
За исключением оболочки C-shell и небольшого числа ее производных все перечисленные оболочки очень похожи и очень близки к оболочке, определенной в спецификациях Х/Оpen 4.2 и POSIX 1003.2. В спецификации POSIX 1003.2 задан минимум, необходимый для создания командной оболочки, а в спецификации Х/Open представлена более дружественная и мощная оболочка.
Прежде чем заняться подробностями программ командной оболочки, необходимо сказать несколько слов о возможностях перенаправления ввода и вывода программ (не только программ командной оболочки) в ОС Linux.
Возможно, вы уже знакомы с некоторыми видами перенаправления, например, таким как:
$ ls -l > lsoutput.txt
сохраняющим вывод команды ls в файле с именем lsoutput.txt.
Однако перенаправление позволяет сделать гораздо больше, чем демонстрирует этот простой пример. В главе 3 вы узнаете больше о дескрипторах стандартных файлов, а сейчас вам нужно знать только то, что дескриптор файла 0 соответствует стандартному вводу программы, дескриптор файла 1 — стандартному выводу, а дескриптор файла 2 — стандартному потоку ошибок. Каждый из этих файлов можно перенаправлять независимо друг от друга. На самом деле можно перенаправлять и другие дескрипторы файлов, но, как правило, нет нужды перенаправлять любые другие дескрипторы, кроме стандартных: 0, 1 и 2.
В предыдущем примере стандартный вывод перенаправлен в файл с помощью оператора >
. По умолчанию, если файл с заданным именем уже есть, он будет перезаписан. Если вы хотите изменить поведение по умолчанию, можно применить команду set -о noclobber
(или set -С
), которая устанавливает опцию noclobber
, чтобы помешать перезаписи при перенаправлении. Отменить эту опцию можно с помощью команды set +о noclobber
. Позже в этой главе будут приведены другие опции команды set
.
Для дозаписи в конец файла используйте оператор >>
. Например, команда
$ ps >> lsoutput.txt
добавит вывод команды ps
в конец заданного файла.
Для перенаправления стандартного потока ошибок перед оператором >
вставьте номер дескриптора файла, который хотите перенаправить. Поскольку у стандартного потока ошибок дескриптор файла 2, укажите оператор 2>
. Часто бывает полезно скрывать стандартный поток ошибок, запрещая вывод его на экран.
Предположим, что вы хотите применить команду kill
для завершения процесса из сценария. Всегда существует небольшой риск, что процесс закончится до того, как выполнится команда kill
. Если это произойдет, команда kill выведет сообщение об ошибке в стандартный поток ошибок, который по умолчанию появится на экране. Перенаправив стандартный вывод команды и ошибку, вы сможете помешать команде kill
выводить какой бы то ни было текст на экран.
Команда
$ kill -HUP 1234 >killout. txt 2>killer.txt
поместит вывод и информацию об ошибке в разные файлы.
Если вы предпочитаете собрать оба набора выводимых данных в одном файле, можно применить оператор >&
для соединения двух выводных потоков. Таким образом, команда
$ kill -1 1234 >killerr.txt 2>&1
поместит свой вывод и стандартный поток ошибок в один и тот же файл. Обратите внимание на порядок следования операторов. Приведенный пример читается как "перенаправить стандартный вывод в файл killerr.txt, а затем перенаправить стандартный поток ошибок туда же, куда и стандартный вывод". Если вы нарушите порядок, перенаправление выполнится не так, как вы ожидаете.
Поскольку обнаружить результат выполнения команды kill
можно с помощью кода завершения (который будет подробно обсуждаться далее в этой главе), часто вам не потребуется сохранять какой бы то ни было стандартный вывод или стандартный поток ошибок. Для того чтобы полностью отбросить любой вывод, вы можете использовать универсальную "мусорную корзину" Linux, /dev/null, следующим образом:
$ kill -l 1234 >/dev/null 2>&1
Также как вывод вы можете перенаправить ввод. Например,
$ more < killout.txt
Понятно, что это тривиальнейший пример для ОС Linux; команда more
в системе Linux в отличие от своего эквивалента командной строки в ОС Windows с радостью принимает имена файлов в качестве параметров.
Вы можете соединять процессы с помощью оператора канала (|
). В ОС Linux, в отличие от MS-DOS, процессы, соединенные каналами, могут выполняться одновременно и автоматически переупорядочиваться в соответствии с потоками данных между ними. Как пример, можно применить команду sort
для сортировки вывода команды ps
.
Если не применять каналы, придется использовать несколько шагов, подобных следующим:
$ ps > psout.txt
$ sort psout.txt > pssort.out
Соединение процессов каналом даст более элегантное решение:
$ ps | sort > pssort.out
Поскольку вы, вероятно, захотите увидеть на экране вывод, разделенный на страницы, можно подсоединить третий процесс, more
, и все это в одной командной строке:
$ ps | sort | more
Практически нет ограничений на допустимое количество процессов. Предположим, что вы хотите видеть все имена выполняющихся процессов, за исключением командных оболочек. Можно применить следующую командную строку:
$ ps -хо соmm | sort | uniq | grep -v sh | more
В ней берется вывод команды ps
, сортируется в алфавитном порядке, из него извлекаются процессы с помощью команды uniq
, применяется утилита grep -v sh
для удаления процесса с именем sh
и в завершение полученный список постранично выводится на экран.
Как видите, это гораздо более элегантное решение, чем строка из отдельных команд, каждая со своими временными файлами. Но в этом случае имейте в виду следующее. Если строка состоит из команд, файл вывода создается или записывается сразу, как только сформирован набор команд, поэтому в строке из нескольких команд никогда не используйте дважды одно и то же имя файла. Если вы попытаетесь сделать что-то подобное:
cat mydata.txt | sort | uniq > mydata.txt
то в результате получите пустой файл, т.к. вы перезапишете файл mydata.txt, прежде чем прочтете его.
Теперь, когда вы увидели некоторые базовые операции командной оболочки, самое время перейти к реальным программам оболочки. Есть два способа написания таких программ. Вы можете ввести последовательность команд и разрешить командной оболочке выполнить их в интерактивном режиме или сохранить эти команды в файле и затем запускать его как программу.
Легкий и очень полезный во время обучения или тестирования способ проверить работу небольших фрагментов кода — просто набрать с клавиатуры в командной строке сценарий командной оболочки.
Предположим, что у вас большое количество файлов на языке С, и вы хотите проверить наличие в них строки POSIX
. Вместо того чтобы искать в файлах строку с помощью команды grep
и затем выводить на экран отдельно каждый файл, вы можете выполнить всю операцию в интерактивном сценарии, похожем на следующий:
$ for file in *
> do
> if grep -l POSIX $file
> then
> more $file
> fi
> done
posix
This is a file with POSIX in it - treat it well
$
Обратите внимание на то, как меняется знак $
, стандартная подсказка или приглашение командной оболочки, на символ >
, когда оболочка ожидает очередной ввод. Вы можете продолжить набор, дав оболочке понять, когда закончите, и сценарий немедленно выполнится.
В этом примере команда grep
выводит на экран найденные ею имена файлов, содержащих строку POSIX
, а затем команда more
отображает на экране содержимое файла. В конце на экран возвращается приглашение командной оболочки. Обратите внимание также на то, что вы ввели переменную командной оболочки, которая обрабатывает каждый файл для самодокументирования сценария. С таким же успехом вы могли бы использовать переменную i
, но имя file
более информативно с точки зрения пользователей.
Командная оболочка также обрабатывает групповые символы или метасимволы (часто называемые знаками подстановки). Вы почти наверняка знаете о применении символа *
как знака подстановки, соответствующего строке символов. Но вы можете не знать о существовании односимвольного знака подстановки, ?
, а конструкция [set]
позволяет проверить любое количество одиночных символов, [^set]
— применяет логическую операцию "НЕ" к множеству, т.е. включает все, кроме того, что вы задали. Подстановочный шаблон из фигурных скобок {}
(доступен в некоторых командных оболочках, включая bash) позволяет формировать множество из произвольных строк, которое командная оболочка раскроет. Например, команда
$ ls my_{finger, toe}s
будет выводить файлы my_fingers и my_toes. Эта команда использует оболочку для проверки всех файлов в текущем каталоге. Мы вернемся к этим правилам соответствия шаблонам в конце главы, когда будем более подробно рассматривать утилиту grep
и возможности регулярных выражений.
Опытные пользователи ОС Linux, вероятно, выполнят эту операцию более эффективным способом, возможно, с помощью следующей команды:
$ more `grep -l POSIX *`
или синонимической конструкции
$ more $(grep -l POSIX *)
В дополнение команда
$ grep -l POSIX * | more
выведет на экран имя файла, содержащего строку POSIX
. В этом сценарии мы видим, что командная оболочка для выполнения трудной работы привлекает другие команды, например, grep
и more
. Оболочка просто позволяет собрать вместе несколько имеющихся команд новыми оригинальными способами. В следующих сценариях вы увидите использование знаков подстановки неоднократно. Мы подробно рассмотрим целую область подстановок, когда будем знакомиться с регулярными выражениями в разделе, посвященном команде grep
.
Заниматься этой канителью каждый раз, когда хочешь выполнить последовательность команд, утомительно. Нужно сохранить команды в файле, который принято называть сценарием или скриптом командной оболочки, а затем вы можете выполнять эти файлы, когда захотите.
С помощью любого текстового редактора необходимо создать файл, содержащий команды. Создайте файл с именем first с таким содержимым:
#!/bin/sh
# first
# Этот файл просматривает все файлы в текущем каталоге
# для поиска строки POSIX, а затем выводит имена
# найденных файлов в стандартный вывод.
for file in *
do
if grep -q POSIX $file
then
echo $file
fi
done
exit 0
Комментарий начинается со знака #
и продолжается до конца строки. Принято знак #
ставить в первой символьной позиции строки. Сделав такое общее заявление, далее отметим, что первая строка #!/bin/sh
— это особая форма комментария; символы #!
сообщают системе о том, что следующий за ними аргумент — программа, применяемая для выполнения данного файла. В данном случае программа /bin/sh
— командная оболочка, применяемая по умолчанию.
Обратите внимание на абсолютный путь, заданный в комментарии. Принято сохранять его длиной не более 32 символов для обратной совместимости, поскольку некоторые старые версии ОС UNIX могут использовать только такое ограниченное количество символов в комментарии #!
, хотя у ОС Linux обычно нет подобного ограничения.
Поскольку сценарий по существу обрабатывается как стандартный ввод командной оболочки, он может содержать любые команды ОС Linux, на которые ссылается переменная окружения PATH
.
Команда exit
гарантирует, что сценарий вернет осмысленный код завершения (подробнее об этом чуть позже в данной главе). Он редко проверяется при интерактивном выполнении программ, но если вы хотите запускать данный сценарий из другого сценария и проверять, успешно ли он завершился, возврат соответствующего кода завершения очень важен. Даже если вы не намерены разрешать вашему сценарию запускаться из другого сценария, все равно следует завершать его с подходящим кодом. Верьте в полезность своего сценария: вдруг когда-нибудь он потребуется для многократного использования как часть другого сценария.
В программировании средствами командной оболочки ноль означает успех. Поскольку представленный вариант сценария не может обнаружить какие-либо ошибки, он всегда возвращает код успешного завершения. Мы вернемся к причинам использования нулевого кода завершения для обозначения успешного выполнения программы позже в этой главе, когда будем более подробно обсуждать команду exit
.
В сценарии не используются никакие расширения и суффиксы имен файлов; ОС Linux и UNIX, как правило, редко применяют при именовании файлов расширения для указания типа файла. Вы могли бы использовать расширение sh или любое другое, командную оболочку это не волнует. У большинства предустановленных сценариев нет никакого расширения в именах файлов и лучший способ проверить, сценарий это или нет применить команду
file
, например, file first
или file /bin/bash
. Пользуйтесь любыми правилами, принятыми в вашей организации или удобными для вас.
Теперь, когда у вас есть файл сценария, его можно выполнить двумя способами. Более простой путь — запустить оболочку с именем файла сценария как параметром:
$ /bin/sh first
Этот вариант будет работать, но гораздо лучше запускать сценарий, введя его имя и тем самым присвоив ему статус других команд Linux. Сделаем это с помощью команды chmod, изменив режим файла (file mode) и сделав его исполняемым для всех пользователей:
$ chmod +х first
Конечно, превращение файла в исполняемый — это не единственный вариант применения команды chmod
. Для того чтобы узнать больше о восьмеричных аргументах и других опциях команды, наберите man chmod
.
После этого вы можете выполнять файл с помощью команды
$ first
При этом может появиться сообщение об ошибке, говорящее о том, что команда не найдена. Почти наверняка причина в том, что в переменной PATH
не задан текущий каталог для поиска выполняемых команд. Исправить это можно либо введя с клавиатуры в командной строке PATH=$PATH:.
, либо добавив данную команду в конец файла .bash_profile. Затем выйдите из системы и зарегистрируйтесь снова. В противном случае введите ./first
в каталог, содержащий сценарий, чтобы задать командной оболочке полный относительный путь к файлу.
Указание пути, начинающегося с символов ./
, дает еще одно преимущество: в этом случае вы случайно не сможете выполнить другую команду с тем же именем, что и у вашего файла сценария.
Не следует вносить подобные изменения в переменную PATH
для суперпользователя, как правило, с именем root. Это лазейка в системе безопасности, т.к. системного администратора, зарегистрировавшегося как root, обманным путём могут заставить запустить фиктивную версию стандартной команды. Один из авторов однажды разрешил сделать это — конечно только для того, чтобы поставить перед системным администратором вопрос о безопасности! В случае обычных учетных записей включение текущего каталога в полный путь сопряжено с очень небольшим риском, поэтому, если вам это нужно, примите за правило добавление комбинации символов ./
перед всеми командами, находящимися в локальном каталоге.
После того как вы убедитесь в корректной работе вашего сценария, можете переместить его в более подходящее место, чем текущий каталог. Если команда предназначена только для собственных нужд, можете создать каталог bin в своем исходном каталоге и добавить его в свой путь. Если вы хотите, чтобы сценарий выполняли другие пользователи, можно использовать каталог /usr/local/bin или другой системный каталог как удобное, хранилище для вновь созданных программ. Если в вашей системе у вас нет прав суперпользователя, можно попросить системного администратора скопировать ваш файл для вас, хотя сначала, возможно, придется убедить его в неоспоримых достоинствах вашего файла. Для того чтобы установить владельца и права доступа к файлу, администратору придется задать такую последовательность команд:
# ср first /usr/local/bin
# chown root /usr/local/bin/first
# chgrp root /usr/local/bin/first
# chmod 755 /usr/local/bin/first
Обратите внимание на то, что в данном случае вместо изменения определенной части флагов прав доступа вы используете абсолютную форму команды chmod, потому что точно знаете, какие требуются права доступа.
Если захотите, можно применить более длинную, но более понятную форму команды chmod
:
# chmod u=rwx, go=rx /usr/local/bin/first
Более подробную информацию можно найти в справочном руководстве команды chmod
.
В ОС Linux вы можете удалить файл, если у вас есть право записи в каталог, содержащий этот файл. Для безопасности убедитесь в том, что право записи в каталоги, содержащие файлы, которые вы хотели бы защитить, есть только у суперпользователя. В этом есть смысл, потому что каталог — это всего лишь еще один файл, и наличие права записи в файл каталога позволяет пользователям добавлять и удалять имена.
Теперь, когда мы рассмотрели пример простой программы командной оболочки, пришло время углубиться в функциональные возможности программирования средствами командной оболочки. Командная оболочка — довольно легкий для изучения язык программирования, в немалой степени потому, что легко проверить в интерактивном режиме работу небольших фрагментов программы, прежде чем собирать их в большие сценарии. Командную оболочку bash можно использовать для создания довольно больших структурированных программ. В нескольких последующих разделах мы обсудим такие темы:
□ переменные: строки, числа, переменные окружения и параметры;
□ условия: булевы или логические выражения (Booleans);
□ управление выполнением программы: if
, elif
, for
, while
, until
, case
;
□ списки;
□ функции;
□ команды, встроенные в командную оболочку;
□ получение результата выполнения команды;
□ встроенные (here) документы.
В командной оболочке переменные перед применением обычно не объявляются. Вместо этого вы создаете их, просто используя (например, когда присваиваете им начальное значение). По умолчанию все переменные считаются строками и хранятся как строки, даже когда им присваиваются числовые значения. Командная оболочка и некоторые утилиты преобразуют строки, содержащие числа, в числовые значения, когда нужно их обработать должным образом. Linux — система, чувствительная к регистру символов, поэтому командная оболочка считает foo
и Foo
двумя разными переменными, отличающимися от третьей переменной FOO
.
В командной оболочке можно получить доступ к содержимому переменной, если перед ее именем ввести знак $
. Каждый раз, когда вы извлекаете содержимое переменной, вы должны к началу ее имени добавить знак $
. Когда вы присваиваете переменной значение, просто используйте имя переменной, которая при необходимости будет создана динамически. Легко проверить содержимое переменной, выведя ее на терминал с помощью команды echo
и указав перед именем переменной знак $
.
Вы можете увидеть это в действии, если в командной строке будете задавать и проверять разные значения переменной salutation
:
$ salutation=Hello
$ echo $salutation
Hello
$ salutation="Yes Dear"
$ echo $salutation
Yes Dear
$ salutation=7+5
$ echo $salutation
7+5
Обратите внимание на то, что при наличии пробелов в содержимом переменной ее заключают в кавычки. Кроме того, не может быть пробелов справа и слева от знака равенства.
Вы можете присвоить переменной пользовательский ввод с помощью команды read
. Она принимает один параметр — имя переменной, в которую будут считываться данные, и затем ждет, пока пользователь введет какой-либо текст. Команда read
обычно завершается после нажатия пользователем клавиши <Enter>. При чтении переменной с терминала, как правило, заключать ее значения в кавычки не требуется:
$ read salutation
Wie geht's?
$ echo $salutation
Wie geht's?
Прежде чем двигаться дальше, вам следует уяснить одно правило командной оболочки: использование кавычек.
Обычно параметры в сценариях отделяются неотображаемыми символами или знаками форматирования (например, пробелом, знаком табуляции или символом перехода на новую строку). Если вы хотите, чтобы параметр содержал один или несколько неотображаемых символов, его следует заключить в кавычки.
Поведение переменных, таких как $foo
, заключенных в кавычки, зависит от вида используемых кавычек. Если вы заключаете в двойные кавычки $
-представление переменной, оно во время выполнения командной строки заменяется значением переменной. Если вы заключаете его в одинарные кавычки или апострофы, никакой замены не происходит. Вы также можете отменить специальное назначение символа $
, вставив перед ним символ \
(обратный слэш).
Выполним упражнение 2.1.
В этом упражнении показано, как кавычки влияют на вывод переменной:
#!/bin/sh
myvar="Hi there"
echo $myvar
echo "$myvar"
echo '$myvar'
echo \$myvar
echo Enter some text
read myvar
echo '$myvar' now equals $myvar
exit 0
Данный сценарий ведет себя следующим образом:
$ ./variable
Hi there
Hi there
$myvar
$myvar
Enter some text
Hello World
$myvar now equals Hello World
Как это работает
Создается переменная myvar
, и ей присваивается строка Hi there
. Содержимое переменной выводится на экран с помощью команды echo
, демонстрирующей, как символ $
раскрывает содержимое переменной. Вы видите, что применение двойных кавычек не влияет на раскрытие содержимого переменной, а одинарные кавычки и обратный слэш влияют. Вы также применяете команду read
для получения строки от пользователя.
Когда стартует сценарий командной оболочки, некоторым переменным присваиваются начальные значения из окружения или рабочей среды. Обычно такие переменные обозначают прописными буквами, чтобы отличать их в сценариях от определенных пользователем переменных (командной оболочки), которые принято обозначать строчными буквами. Формируемые переменные зависят от ваших персональных настроек. Многие из них перечислены на страницах справочных руководств, а основные приведены в табл. 2.2.
Таблица 2.2
Переменная окружения | Описание |
---|---|
$НОМЕ | Исходный каталог текущего пользователя |
$PATH | Разделенный двоеточиями список каталогов для поиска команд |
$PS1 | Подсказка или приглашение командной строки. Часто знак $ , но в оболочке bash можно применять и более сложные варианты. Например, строка [\u@\h \w]$ — популярный стандарт, сообщающий в подсказке пользователя имя компьютера и текущий каталог, а также знак $ |
$PS2 | Дополнительная подсказка или приглашение, применяемое как приглашение для дополнительного ввода; обычно знак > |
$IFS | Разделитель полей ввода. Список символов, применяемых для разделения слов при чтении оболочкой ввода, как правило, пробел, знак табуляции и символ перехода на новую строку |
$0 | Имя сценария командной оболочки |
$# | Количество передаваемых параметров |
$$ | ID (идентификатор) процесса сценария оболочки, часто применяемый внутри сценария для генерации уникальных имен временных файлов; например, /tmp/tmpfile_$$ |
Если вы хотите проверить с помощью команды env <команда>
, как работает программа в разных окружениях, познакомьтесь с интерактивным справочным руководством к команде env
. Далее в этой главе вы увидите, как задавать переменные окружения в подоболочках (subshells), применяя команду export
.
Если ваш сценарий вызывается с параметрами, создается несколько дополнительных переменных. Если параметры не передаются, переменная окружения $#
все равно существует, но равна 0.
Переменные-параметры перечислены в табл. 2.3.
Таблица 2.3
Переменная-параметр | Описание |
---|---|
$1 , $2 , ... | Параметры, передаваемые сценарию |
$* | Список всех параметров в единственной переменной, разделенных первым символом из переменной окружения IFS . Если IFS корректируется, способ разделения командной строки на параметры в переменной $* изменяется |
$@ | Едва различимая вариация $* ; не использует переменную окружения IFS , поэтому параметры не сольются, даже если переменная IFS пуста |
Легче всего увидеть разницу между переменными-параметрами $*
и $@
, опробовав их.
$ IFS=''
$ set foo bar bam
$ echo "$@"
foo bar bam
$ echo "$*"
foobarbam
$ unset IFS
$ echo "$*"
foo bar bam
Как видите, заключенная в двойные кавычки переменная-параметр $@
представляет позиционные параметры как отдельные поля, независимо от значения переменной окружения IFS
. Как правило, если вы хотите получить доступ к параметрам, лучше использовать переменную-параметр.
Помимо вывода на экран содержимого переменных с помощью команды echo
, вы также можете прочитать его командой read
(упражнение 2.2).
В приведенном далее сценарии показано несколько простых манипуляций переменными. После ввода сценария и записи его в файл try_var не забудьте превратить его в исполняемый файл с помощью команды chmod +х try_var
.
#!/bin/sh
salutation="Hello"
echo $salutation
echo "The program $0 is now running"
echo "The second parameter was $2"
echo "The first parameter was $1"
echo "The parameter list was
echo "The user's home directory is $HOME"
echo "Please enter a new greeting"
read salutation
echo $salutation
echo "The script is now complete"
exit 0
Если вы выполните этот сценарий, то получите следующий вывод:
$ ./try_var foo bar baz
Hello
The program ./try_var is now running
The second parameter was bar
The first parameter was foo
The parameter list was foo bar baz
The user's home directory is /home/rick
Please enter a new greeting
Sire
Sire
The script is now complete $
Как это работает
Сценарий создает переменную salutation
, выводит на экран ее содержимое и затем показывает, что уже сформированы и имеют соответствующие значения различные переменные-параметры и переменная окружения $НОМЕ
.
Далее в этой главе мы рассмотрим более подробно подстановку параметров.
Основа всех языков программирования — средства проверки условий и выполнение различных действий с учетом результатов этой проверки. Но прежде чем говорить об этом, давайте рассмотрим условные конструкции, которые можно применять в сценариях командной оболочки, а затем познакомимся с использующими их управляющими структурами.
Сценарий командной оболочки может проверить код завершения любой команды, вызванной из командной строки, включая сценарии, написанные вами. Вот почему так важно всегда включать в создаваемые вами сценарии команду exit с определенным значением.
На практике в большинстве сценариев широко используется команда [
или test
— логическая проверка командной оболочки. В некоторых системах команды [
и test
— синонимы, за исключением того, что при использовании команды [
для удобочитаемости применяется и завершающая часть ]
. Наличие команды [
может показаться странным, но в программном коде она упрощает синтаксис и делает его более похожим на другие языки программирования.
Эти команды в некоторых ранних оболочках UNIX вызывают внешнюю программу, но в более современных версиях их стараются делать встроенными. Мы вернемся к этому, когда будем рассматривать команды в следующем разделе.
Поскольку команда test
не часто применяется за пределами сценариев командной оболочки, многие пользователи ОС Linux, никогда раньше не писавшие сценариев, пытаются создавать простые программы и называют их test. Если такая программа не работает, вероятно, она конфликтует с командой оболочки test
. Для того чтобы выяснить, есть ли в вашей системе внешняя команда с данным именем, попытайтесь набрать что-нибудь вроде which test
и проверить, какая именно команда test
выполняется в данный момент, или используйте форму ./test
, чтобы быть уверенным в том, что вы выполняете сценарий из текущего каталога. Если сомневаетесь, примите за правило выполнять свои сценарии, предваряя при запуске их имена комбинацией символов ./
.
Мы представим команду test
на примере одного простейшего условия: проверки наличия файла. Для нее понадобится следующая команда: test -f <имя_файла>
, поэтому в сценарии можно написать
if test -f fred.c
then
...
fi
To же самое можно записать следующим образом:
if [ -f fred.c ]
then
...
fi
Код завершения команды test
(выполнено ли условие) определяет, будет ли выполняться условный программный код.
Имейте в виду, что вы должны вставлять пробелы между квадратной скобкой [
и проверяемым условием. Это легко усвоить, если запомнить, что вставить символ [
— это все равно, что написать test
, а после имени команды вы всегда должны вставлять пробел.
Если вы предпочитаете помещать слово then
в той же строке, что и if
, нужно добавить точку с запятой для отделения команды test
от then
:
if [ -f fred.c ]; then
...
fi
Варианты условий, которые вы можете применять в команде test
, делятся на три типа: строковые сравнения, числовые сравнения и проверка файловых флагов (file conditionals). Эти типы условий описаны в табл. 2.4.
Таблица 2.4
Варианты условий | Результат |
---|---|
Сравнения строк | |
Строка1 = Строка2 | True (истина), если строки одинаковы |
Строка1 != Строка2 | True (истина), если строки разные |
-n Строка | True (истина), еслиСтрока не null |
-z Строка | True (истина), еслиСтрока null (пустая строка) |
Сравнения чисел | |
Выражение1 -eq Выражение2 | True (истина), если выражения равны |
Выражение1 -ne Выражение2 | True (истина), если выражения не равны |
Выражение1 -gt Выражение2 | True (истина), еслиВыражение1 больше, чемВыражение2 |
Выражение1 -ge Выражение2 | True (истина), еслиВыражение1 не меньшеВыражение2 |
Выражение1 -lt Выражение2 | True (истина), еслиВыражение1 меньше, чемВыражение2 |
Выражение1 -lе Выражение2 | True (истина), еслиВыражение1 не большеВыражение2 |
! Выражение | True (истина), еслиВыражение ложно, и наоборот |
Файловый флаг | |
-d файл | True (истина), еслифайл — каталог |
-е файл | True (истина), еслифайл существует. Исторически, опция -e не была переносима на другие платформы, поэтому обычно применяется -f |
-f файл | True (истина), еслифайл — обычный файл |
-g файл | True (истина), если дляфайла установлен бит set-group-id |
-r файл | True (истина), если файл доступен для чтения |
-s файл | True (истина), если файл ненулевого размера |
-u файл | True (истина), если дляфайла установлен бит set-user-id |
-v файл | True (истина), еслифайл доступен для записи |
-х файл | True (истина), если файл — исполняемый файл |
Вас могли заинтересовать непонятные биты set-group-id и set-user-id (также называемые set-gid и set-uid). Бит set-uid предоставляет программе права владельца, а не просто ее пользователя, бит set-gid предоставляет программе права группы. Эти биты устанавливаются командой chmod
с помощью опций s
и g
. На файлы, содержащие сценарии, флаги set-gid и set-uid не влияют, они оказывают влияние только на исполняемые двоичные файлы.
Мы немного сами себя обогнали, но далее следует пример тестирования состояния файла /bin/bash, так что вы сможете увидеть, как это выглядит на практике.
#!/bin/sh
if [ -f /bin/bash ]
then
echo "file /bin/bash exists"
fi
if [ -d /bin/bash ]
then
echo "/bin/bash is a directory"
else
echo "/bin/bash is NOT a directory"
fi
Для того чтобы тест мог оказаться истинным, предварительно, для проверки всех файловых флагов требуется наличие файла. Данный перечень включает только самые широко используемые опции команды test
, полный список можно найти в интерактивном справочном руководстве. Если вы применяете оболочку bash, в которую встроена команда test
, используйте команду help test
для получения дополнительных сведений. Позже в этой главе мы применим некоторые из этих опций.
Теперь, когда вы познакомились с условиями, можно рассмотреть управляющие структуры, использующие эти условия.
В командной оболочке есть ряд управляющих структур или конструкций, похожих на аналогичные структуры в других языках программирования.
В следующих разделах элемент синтаксической записи операторы — это последовательности команд, которые выполняются, когда или пока условие удовлетворяется или пока оно не удовлетворяется.
Управляющий оператор if
очень прост: он проверяет результат выполнения команды и затем в зависимости отусловия
выполняет ту или иную группу операторов.
ifусловие
then
операторы
else
операторы
fi
Наиболее часто оператор if
применяется, когда задается вопрос, и решение принимается в зависимости от ответа:
#!/bin/sh
echo "Is it morning? Please answer yes or no "
read timeofday
if [ $timeofday = "yes" ]; then
echo "Good morning"
else
echo "Good afternoon"
fi
exit 0
В результате будет получен следующий вывод на экран:
Is it morning? Please answer yes or no
yes
Good morning
$
В этом сценарии для проверки содержимого переменной timeofday
применяется команда [
. Результат оценивается оператором командной оболочки if
, который затем разрешает выполнять разные строки программного кода.
Обратите внимание на дополнительные пробелы, используемые для формирования отступа внутри оператора if
. Это делается только для удобства читателя; командная оболочка игнорирует дополнительные пробелы.
К сожалению, с этим простым сценарием связано несколько проблем. Во-первых, он принимает в значении no
(нет) любой ответ за исключением yes
(да). Можно помешать этому, воспользовавшись конструкцией elif
, которая позволяет добавить второе условие, проверяемое при выполнении части else
оператора if
(упражнение 2.3).
Вы можете откорректировать предыдущий сценарий так, чтобы он выводил сообщение об ошибке, если пользователь вводит что-либо отличное от yes
или no
. Для этого замените ветку else
веткой elif
и добавьте еще одно условие:
#!/bin/sh
echo "Is it morning? Please answer yes or no "
read timeofday
if [ $timeofday = "yes" ]
then
echo "Good morning"
elif [ $timeofday = "no" ]; then
echo "Good afternoon"
else
echo "Sorry, $timeofday not recognized. Enter yes or no "
exit 1
fi
exit 0
Как это работает
Этот пример очень похож на предыдущий, но теперь, если первое условие не равно true
, оператор командной оболочки elif
проверяет переменную снова. Если обе проверки не удачны, выводится сообщение об ошибке, и сценарий завершается со значением 1, которое в вызывающей программе можно использовать для проверки успешного выполнения сценария.
Данный сценарий исправляет наиболее очевидный дефект, а более тонкая проблема остается незамеченной. Запустите новый вариант сценария, но вместо ответа на вопрос просто нажмите клавишу <Enter> (или на некоторых клавиатурах клавишу <Return>). Вы получите сообщение об ошибке:
[: =: unary operator expected
Что же не так? Проблема в первой ветви оператора if
. Когда проверялась переменная timeofday
, она состояла из пустой строки. Следовательно, ветвь оператора if
выглядела следующим образом:
if [ = "yes" ]
и не представляла собой верное условие. Во избежание этого следует заключить имя переменной в кавычки:
if [ "$timeofday" = "yes" ]
Теперь проверка с пустой переменной будет корректной:
if [ "" = "yes" ]
Новый сценарий будет таким:
#!/bin/sh
echo "Is it morning? Please answer yes or no "
read timeofday
if [ "$timeofday" = "yes" ]
then
echo "Good morning"
elif [ "$timeofday" = "no" ]; then
echo "Good afternoon"
else
echo "Sorry, $timeofday not recognized. Enter yes or no "
exit 1
fi
exit 0
Этот вариант безопасен, даже если пользователь в ответ на вопрос просто нажмет клавишу <Enter>.
Если вы хотите, чтобы команда echo
удалила новую строку в конце, наиболее легко переносимый вариант — применить команду printf
(см. разд. "printf" далее в этой главе) вместо команды echo
. В некоторых командных оболочках применяется команда echo -е
, но она поддерживается не всеми системами. В оболочке bash для запрета перехода на новую строку допускается команда echo -n
, поэтому, если вы уверены, что вашему сценарию придется трудиться только в оболочке bash, предлагаем вам использовать следующий синтаксис:
echo -n "Is it morning? Please answer yes or no: "
Помните о том, что нужно оставлять дополнительный пробел перед закрывающими кавычками, таким образом формируется зазор перед вводимым пользователем ответом, который в этом случае выглядит четче.
Применяйте конструкцию for
для обработки в цикле ряда значений, которые могут представлять собой любое множество строк. Строки могут быть просто перечислены в программе или, что бывает чаще, представлять собой результат выполненной командной оболочкой подстановки имен файлов.
Синтаксис этого оператора прост:
forпеременная inзначения
do
операторы
done
Выполните упражнения 2.4 и 2.5.
В командной оболочке значения обычно представлены в виде строк, поэтому можно написать следующий сценарий:
#!/bin/sh
for foo in bar fud 43
do
echo $foo
done
exit 0
В результате будет получен следующий вывод:
bar
fud
43
Что произойдет, если вы измените первую строку с for foo in bar fud 43
на for foo in "bar fud 43"
? Напоминаем, что вставка кавычек заставляет командную оболочку считать все, что находится между ними, единой строкой. Это один из способов сохранения пробелов в переменной.
Как это работает
В данном примере создается переменная foo
и ей в каждом проходе цикла for
присваиваются разные значения. Поскольку оболочка считает по умолчанию все переменные строковыми, применять строку 43 так же допустимо, как и строку fud
.
Как упоминалось ранее, цикл for
обычно используется в командной оболочке вместе с метасимволами или знаками подстановки для имен файлов. Это означает применение метасимвола для строковых значений и предоставление оболочке возможности подставлять все значения на этапе выполнения.
Вы уже видели этот прием в первом примере first. В сценарии применялись средства подстановки командной оболочки — символ *
для подстановки имен всех файлов из текущего каталога. Каждое из этих имен по очереди используется в качестве значения переменной $file
внутри цикла for
.
Давайте бегло просмотрим еще один пример подстановки с помощью метасимвола. Допустим, что вы хотите вывести на экран все имена файлов сценариев в текущем каталоге, начинающиеся с буквы "f", и вы знаете, что имена всех ваших сценариев заканчиваются символами .sh. Это можно сделать следующим образом:
#!/bin/sh
for file in $(ls f*.sh); do
lpr $file
done
exit 0
Как это работает
В этом примере показано применение синтаксической конструкции $(команда)
, которая будет подробно обсуждаться далее (в разделе, посвященном выполнению команд). Обычно список параметров для цикла for
задается выводом команды, включенной в конструкцию $()
.
Командная оболочка раскрывает f*.sh
, подставляя имена всех файлов, соответствующих данному шаблону.
Помните о том, что все подстановки переменных в сценариях командной оболочки делаются во время выполнения сценария, а не в процессе их написания, поэтому все синтаксические ошибки в объявлениях переменных обнаруживаются только на этапе выполнения, как было показано ранее, когда мы заключали в кавычки пустые переменные.
Поскольку по умолчанию командная оболочка считает все значения строками, оператор for
хорош для циклической обработки наборов строк, но не слишком удобен, если вы не знаете заранее, сколько раз придется его выполнить.
Если нужно повторить выполнение последовательности команд, но заранее не известно, сколько раз следует их выполнить, вы, как правило, будете применять цикл while
со следующей синтаксической записью:
while условие
do
операторы
done
Далее приведен пример довольно слабой программы проверки паролей.
#!/bin/sh
echo "Enter password"
read trythis
while [ "$trythis" != "secret" ]; do
echo "Sorry, try again"
read trythis
done
exit 0
Следующие строки могут служить примером вывода данного сценария:
Enter password
password
Sorry, try again
secret
$
Ясно, что это небезопасный способ выяснения пароля, но он вполне подходит для демонстрации применения цикла while
. Операторы, находящиеся между операторами do
и done
, выполняются бесконечное число раз до тех пор, пока условие остается истинным (true
). В данном случае вы проверяете, равно ли значение переменной trythis
строке secret
. Цикл будет выполняться, пока $trythis
не равно secret
. Затем выполнение сценария продолжится с оператора, следующего сразу за оператором done
.
У цикла until
следующая синтаксическая запись:
until условие
do
операторы
done
Она очень похожа на синтаксическую запись цикла while
, но с обратным проверяемым условием. Другими словами, цикл продолжает выполняться, покаусловие
не станет истинным (true).
Как правило, если нужно выполнить цикл хотя бы один раз, применяют цикл while
; если такой необходимости нет, используют цикл until
.
Как пример цикла until
можно установить звуковой сигнал предупреждения, инициируемый во время регистрации нового пользователя, регистрационное имя которого передается в командную строку.
#!/bin/bash
until who | grep "$1" > /dev/null
do
sleep 60
done
# Теперь звонит колокольчик и извещает о новом пользователе
echo -е '\а'
echo "**** $1 has just logged in ****"
exit 0
Если пользователь уже зарегистрировался в системе, выполнять цикл нет необходимости. Поэтому естественно выбрать цикл until
, а не цикл while
.
Оператор case
немного сложнее уже рассмотренных нами операторов. У него следующая синтаксическая запись:
case переменная in
образец [ | образец] ...) операторы;;
образец [ | образец] ...) операторы;;
esac
Конструкция оператора case
выглядит слегка устрашающей, но она позволяет довольно изощренным способом сопоставлять содержимое переменной с образцами и затем выполнять разные операторы в зависимости от того, с каким образцом найдено соответствие. Это гораздо проще, чем проверять несколько условий, применяемых во множественных операторах if
, elif
и else
.
Обратите внимание на то, что каждая ветвь с образцами завершается удвоенным символом "точка с запятой" (;;
). В каждой ветви оператора case
можно поместить несколько операторов, поэтому сдвоенная точка с запятой необходима для отметки завершения очередного оператора и начала следующей ветви с новым образцом в операторе case
.
Возможность сопоставлять многочисленные образцы и затем выполнять множественные связанные с образцом операторы делают конструкцию case
очень удобной для обработки пользовательского ввода. Лучше всего увидеть, как работает конструкция case
на примерах. Мы будем применять ее в упражнениях 2.6–2.8, каждый раз совершенствуя сопоставление с образцами.
Применяя конструкцию case
с метасимволами в образцах, такими как *
, будьте особенно внимательны. Проблема заключается в том, что принимается во внимание первое найденное соответствие образцу, несмотря на то, что в последующих ветвях могут быть образцы с более точным соответствием.
Вы можете написать новую версию сценария проверки пользовательского ввода с помощью конструкции case
, сделав сценарий немного более избирательным и терпимым к неожиданным вариантам ввода.
#!/bin/sh
echo "Is it morning? Please answer yes or no "
read timeofday
case "$timeofday" in
yes) echo "Good Morning";;
no ) echo "Good Afternoon";;
y ) echo "Good Morning";;
n ) echo "Good Afternoon";;
* ) echo "Sorry, answer not recognized";;
esac
exit 0
Как это работает
Когда выполняется оператор case
, он берет содержимое переменной timeofday
и сравнивает его поочередно с каждой строкой-образцом. Как только строка совпадает с введенной информацией, оператор case
выполняет код, следующий за )
, и завершается.
Оператор case
выполняет обычную подстановку в строках, которые он использует для сравнения. Следовательно, вы можете задать часть строки с последующим метасимволом *
. Применение единственного символа *
будет соответствовать совпадению с любой введенной строкой, поэтому поместите этот вариант после всех остальных образцов строк для того, чтобы задать некоторое стандартное поведение оператора case
, если не будут найдены совпадения с другими строками-образцами. Это возможно, потому что оператор case
сравнивает с каждой строкой-образцом поочередно. Он не ищет наилучшее соответствие, а всего лишь первое встретившееся. Условие, принятое по умолчанию, часто оказывается невыполнимым, поэтому применение метасимвола *
может помочь в отладке сценариев.
Предыдущая версия конструкции case, безусловно, элегантнее варианта с множественными операторами if
, но, объединив все образцы, можно создать более красивую версию.
#!/bin/sh
echo "Is it morning? Please answer yes or no "
read timeofday
case "$timeofday" in
yes | y | Yes | YES ) echo "Good Morning";;
n* | N*) echo "Good Afternoon";;
* ) echo "Sorry, answer not recognized";;
esac
exit 0
Как это работает
Данный сценарий в операторе case
использует несколько строк-образцов в каждой ветви, таким образом, case
проверяет несколько разных строк для каждого возможного оператора. Этот прием делает сценарий короче и, как показывает практика, облегчает его чтение. Приведенный программный код также показывает, как можно использовать метасимвол *
, несмотря на то, что он может соответствовать непредусмотренным образцам. Например, если пользователь введет строку never
, она будет соответствовать образцу n*
, и на экран будет выведено приветствие Good Afternoon (Добрый день), хотя такое поведение в сценарии не предусматривалось. Учтите также, что заключенный в кавычки знак подстановки *
не действует.
В заключение, для того чтобы сделать сценарий многократно используемым, вам необходимо использовать другое значение кода завершения в том случае, когда применяется образец по умолчанию для непонятного варианта ввода.
#!/bin/sh
echo "Is it -morning? Please answer yes or no"
read timeofday
case "$timeofday" in
yes | y | Yes | YES )
echo "Good Morning"
echo "Up bright and early this morning"
;;
[nN]*)
echo "Good Afternoon"
;;
*)
echo "Sorry, answer not recognized"
echo "Please answer yes or no"
exit 1
;;
esac
exit 0
Как это работает
Для демонстрации другого способа определения соответствия образцу в этом программном коде изменен вариант определения соответствия для ветви no
. Также видно, как в каждой ветви оператора case может выполняться несколько операторов. Следует быть внимательным и располагать в операторе самые точные образцы строк первыми, а самые общие варианты образцов последними. Это очень важно, потому что оператор case
выполняется, как только найдено первое, а не наилучшее соответствие. Если вы поставите ветвь *)
первой, совпадение с этим образцом будет определяться всегда, независимо от варианта введенной строки.
Учтите, что сдвоенная точка с запятой ;;
перед служебным словом esac
необязательна. В отличие от программирования на языке С, в котором пропуск маркера завершения считается плохим стилем программирования, пропуск ;;
не создает проблем, если последняя ветвь оператора case
— это вариант, принятый по умолчанию, поскольку другие образцы уже не будут анализироваться.
Для того чтобы сделать средства установления соответствия образцам более мощными, можно применять следующие строки-образцы:
[yY] | [Yy][Ее][Ss])
В них ограничен набор разрешенных букв, но при этом допускаются разнообразные ответы и предлагается более строгий контроль, чем при применении метасимвола *
.
Иногда может понадобиться сформировать последовательность команд. Например, вы хотите выполнить оператор, только если удовлетворяется несколько условий.
if [ -f this_file ]; then
if [ -f that_file ]; then
if [ -f the_other_file ]; then
echo "All files present, and correct"
fi
fi
fi
Или вы хотите, чтобы хотя бы одно условие из последовательности условий было истинным.
if [ -f this_file ]; then
foo="True"
elif [ -f that_file ]; then
foo="True"
elif [ -f the_other_file ];
then foo="True"
else
foo="False"
fi
if ["$foo" = "True" ]; then
echo "One of the files exists"
fi
Несмотря на то, что это можно реализовать с помощью нескольких операторов if
, как видите, результаты получаются очень громоздкими. В командной оболочке есть пара специальных конструкций для работы со списками команд: И-список (AND list) и ИЛИ-список (OR list). Обе они часто применяются вместе, но мы рассмотрим синтаксическую запись каждой из них отдельно.
Эта конструкция позволяет выполнять последовательность команд, причем каждая последующая выполняется только при успешном завершении предыдущей. Синтаксическая запись такова:
оператор1 && оператор2 && оператор3 && ...
Выполнение операторов начинается с самого левого, если он возвращает значение true
(истина), выполняется оператор, расположенный справа от первого оператора. Выполнение продолжается до тех пор, пока очередной оператор не вернет значение false
(ложь), после чего никакие операторы списка не выполняются. Операция &&
проверяет условие предшествующей команды.
Каждый оператор выполняется независимо, позволяя соединять в одном списке множество разных команд, как показано в приведенном далее сценарии. И-список успешно обрабатывается, если все команды выполнены успешно, в противном случае его обработка заканчивается неудачно.
Выполните упражнение 2.9.
В следующем сценарии вы обращаетесь к файлу file_one (для проверки его наличия, и если файл не существует, создаете его) и затем удаляете файл file_two. Далее И-список проверяет наличие каждого файла и между делом выводит на экран кое-какой текст.
#!/bin/sh
touch file_one
rm -f file_two
if [ -f file_one ] && echo "hello" [ -f file_two ] && echo " there"
then
echo "in if"
else
echo "in else"
fi
exit 0
Попробуйте выполнить сценарий, и вы получите следующий вывод:
hello
in else
Как это работает
Команды touch
и rm
гарантируют, что файлы в текущем каталоге находятся в известном состоянии. Далее И-список выполняет команду [ -f file one ]
, которая возвращает значение true
, потому что вы только что убедились в наличии файла. Поскольку предыдущий оператор завершился успешно, теперь выполняется команда echo
. Она тоже завершается успешно (echo
всегда возвращает true
). Затем выполняется третья проверка [ -f file_two ]
. Она возвращает значение false
, т.к. файл не существует. Поскольку последняя команда вернула false
, заключительная команда echo
не выполняется. В результате И-список возвращает значение false
, поэтому в операторе if
выполняется вариант else
.
Эта конструкция позволяет выполнять последовательность команд до тех пор, пока одна из них не вернет значение true, и далее не выполняется ничего более. У нее следующая синтаксическая запись:
оператор1 || оператор2 || оператор3 || ...
Операторы выполняются слева направо. Если очередной оператор возвращает значение false
, выполняется следующий за ним оператор. Это продолжается до тех пор, пока очередной оператор не вернет значение true
, после этого никакие операторы уже не выполняются.
ИЛИ-список очень похож на И-список, за исключением того, что правило для выполнения следующего оператора — выполнение предыдущего оператора со значением false
.
Рассмотрим упражнение 2.10.
Скопируйте сценарий из предыдущего упражнения и измените затененные строки следующим образом.
#!/bin/sh
rm -f file_one
if [ -f file_one ] || echo "hello" || echo " there" then
echo "in if"
else
echo "in else"
fi
exit 0
В результате выполнения данного сценария будет получен следующий вывод:
hello
in if
Как это работает
В первых двух строках просто задаются файлы для остальной части сценария. Первая команда списка [ -f file one ]
возвращает значение false
, потому что файла в каталоге нет. Далее выполняется команда echo
. Вот это да — она возвращает значение true
, и больше в ИЛИ-списке не выполняются никакие команды. Оператор if
получает из списка значение true, поскольку одна из команд ИЛИ-списка (команда echo
) вернула это значение.
Результат, возвращаемый обоими этими списками, — это результат последней выполненной команды списка.
Описанные конструкции списков выполняются так же, как аналогичные конструкции в языке С, когда проверяются множественные условия. Для определения результата выполняется минимальное количество операторов. Операторы, не влияющие на конечный результат, не выполняются. Обычно этот подход называют оптимизацией вычислений (short circuit evaluation).
Комбинирование этих двух конструкций — высшее блаженство для любителей логических задач. Попробуйте проанализировать следующий список:
[ -f file_one ] && команда в случае true || команда в случае false
В нем будет выполняться первая команда в случае истинности проверки и вторая команда в противном случае. Всегда лучше всего поэкспериментировать с этими довольно необычными списками, и, как правило, вам придется использовать скобки для изменения порядка вычислений.
Если вы хотите применить несколько операторов в том месте программного кода, где разрешен только один, например в ИЛИ-списке или И-списке, то можете сделать это, заключив операторы в фигурные скобки {}
и создав тем самым операторный блок. Например, в приложении, представленном далее в этой главе, вы увидите следующий фрагмент программного кода:
get_confirm && {
grep -v "$cdcatnum" $tracks_file > $temp_file
cat $temp_file > $tracks_file
echo
add record_tracks
}
В командной оболочке можно определять функции, и, если вы пишете сценарии любого размера, функции можно применять для структурирования кода.
Как альтернативу можно использовать разбиение большого сценария на много маленьких, каждый из которых выполняет небольшую задачу. У этого подхода есть несколько недостатков: выполнение вложенного в сценарий другого сценария будет гораздо медленнее, чем выполнение функции. Значительно труднее возвращать результаты, и может появиться большое количество маленьких сценариев. Следует рассмотреть наименьшую практически самостоятельную часть вашего сценария и использовать ее как эталон для того, чтобы определить, когда возникает необходимость разбиения большого сценария на коллекцию меньших по размеру сценариев.
Для определения функции в командной оболочке просто введите ее имя и следом за ним пустые круглые скобки, а операторы тела функции заключите в фигурные скобки.
Имя_функции() {
операторы
}
Выполните упражнения 2.11 и 2.12.
Давайте начнем с действительно простой функции.
#!/bin/sh
foo() {
echo "Function foo is executing"
}
echo "script starting"
foo
echo "script ended"
exit 0
Выполняющийся сценарий, выведет на экран следующий текст:
script starting
Function foo is executingscript ended
Как это работает
Данный сценарий начинает выполняться с первой строки. Таким образом, ничего необычного нет, но, когда он находит конструкцию foo() {
, он знает, что здесь дается определение функции, названной foo
. Он запоминает ссылку на функцию и foo продолжает выполнение после обнаружения скобки }
. Когда выполняется строка с единственным именем foo
, командная оболочка знает, что нужно выполнить предварительно определенную функцию. Когда функция завершится, выполнение сценария продолжится в строке, следующей за вызовом функции foo
.
Вы должны всегда определить функцию прежде, чем сможете ее запустить, немного похоже на стиль, принятый в языке программирования Pascal, когда вызову функции предшествует ее определение, за исключением того, что в командной оболочке нет опережающего объявления (forward) функции. Это ограничение не создает проблем, потому что все сценарии выполняются с первой строки, поэтому если просто поместить все определения функций перед первым вызовом любой функции, все функции окажутся определенными до того, как будут вызваны.
Когда функция вызывается, позиционные параметры сценария $*
, $@
, $#
, $1
, $2
и т.д. заменяются параметрами функции. Именно так вы считываете параметры, передаваемые функции. Когда функция завершится, они восстановят свои прежние значения.
Некоторые более ранние командные оболочки не могут восстанавливать значения позиционных параметров после выполнения функций. Не стоит полагаться на описанное в предыдущем абзаце поведение, если вы хотите, чтобы ваши сценарии были переносимыми.
Вы можете заставить функцию возвращать числовые значения с помощью команды return
. Обычный способ возврата функцией строковых значений — сохранение строки в переменной, которую можно использовать после завершения функции. Другой способ — вывести строку с помощью команды echo
и перехватить результат, как показано далее.
foo() { echo JAY;}
...
result="$(foo)"
Вы можете объявлять локальные переменные в функциях командной оболочки с помощью ключевого слова local
. В этом случае переменная действительна только в пределах функции. В других случаях функция может обращаться к переменным командной оболочки, у которых глобальная область действия. Если у локальной переменной то же имя, что и у глобальной, в пределах функции локальная переменная перекрывает глобальную. Для того чтобы убедиться в этом на практике, можно изменить предыдущий сценарий следующим образом.
#!/bin/sh
sample_text="global variable"
foo() {
local sample_text="local variable"
echo "Function foo is executing"
echo $sample_text
}
echo "script starting"
echo $sample_text
foo
echo "script ended"
echo $sample_text
exit 0
При отсутствии команды return
, задающей возвращаемое значение, функция возвращает статус завершения последней выполненной команды,
В следующем сценарии, my_name, показано, как в функцию передаются параметры и как функции могут вернуть логический результат true
или false
. Вы можете вызвать этот сценарий с параметром, задающим имя, которое вы хотите использовать в вопросе.
1. После заголовка командной оболочки определите функцию yes_or_no
.
#!/bin/sh
yes_or_no() {
echo "Is your name $* ? "
while true
do
echo -n "Enter yes or no: "
read x
case "$x" in
y | yes ) return 0;;
n | no ) return 1;;
* ) echo "Answer yes or no"
esac
done
}
2. Далее начинается основная часть программы.
echo "Original parameters are $*"
if yes_or_no "$1"
then
echo "Hi $1, nice name"
else
echo "Never mind"
fi
exit 0
Типичный вывод этого сценария может выглядеть следующим образом:
$ ./my_name Rick Neil
Original parameters are Rick Neil
Is your name Rick ?
Enter yes or no: yes
Hi Rick, nice name
$
Как это работает
Когда сценарий начинает выполняться, функция определена, но еще не выполняется. В операторе if
сценарий вызывает функцию yes_or_no
, передавая ей оставшуюся часть строки как параметры после замены $1
первым параметром исходного сценария строкой Rick
. Функция использует эти параметры, в данный момент хранящиеся в позиционных параметрах $1
, $2
и т.д., и возвращает значение в вызывающую программу. В зависимости от возвращенного функцией значения конструкция if
выполняет один из операторов.
Как видите, у командной оболочки есть большой набор управляющих структур и условных операторов. Вам необходимо познакомиться с некоторыми командами, встроенными в оболочку; после этого вы будете готовы решать реальные программистские задачи без компилятора под рукой!
В сценариях командной оболочки можно выполнять два сорта команд. Как уже упоминалось, существуют "обычные" команды, которые могут выполняться и из командной строки (называемые внешними командами), и встроенные команды (называемые внутренними командами). Внутренние команды реализованы внутри оболочки и не могут вызываться как внешние программы. Но большинство внутренних команд представлено и в виде автономных программ, это условие — часть требований стандарта POSIX. Обычно, не важно, команда внешняя или внутренняя, за исключением того, что внутренние команды действуют эффективнее.
В этом разделе представлены основные команды, как внутренние, так и внешние, которые мы используем при написании сценариев. Как пользователь ОС Linux, вы, возможно, знаете много других команд, которые принимает командная строка. Всегда помните о том, что вы можете любую из них применить в сценарии в дополнение к встроенным командам, представленным в данном разделе.
Используйте команду break
для выхода из циклов for
, while
и until
до того, как будет удовлетворено управляющее условие. В команде break
можно задать дополнительный числовой параметр, указывающий на число циклов, из которых предполагается выход. Однако это может сильно усложнить чтение сценариев, поэтому мы не советуем вам использовать его. По умолчанию break
обеспечивает выход из одного цикла.
#!/bin/sh
rm -rf fred*
echo > fred1
echo > fred2
mkdir fred3
echo > fred4
for file in fred*
do
if [ -d "$file" ]; then
break;
fi
done
echo first directory starting fred was $file
m -rf fred*
exit 0
Команда "двоеточие" — фиктивная команда. Она иногда полезна для упрощения логики в условиях, будучи псевдонимом команды true
. Поскольку команда :
встроенная, она выполняется быстрее, чем true
, хотя ее вывод гораздо менее читабелен.
Вы можете найти эту команду в условии для циклов while
. Конструкция while :
выполняет бесконечный цикл вместо более общего while true
.
Конструкция :
также полезна для условного задания переменных. Например,
: ${var:=value}
Без :
командная оболочка попытается интерпретировать $var
как команду.
В некоторых более старых версиях сценариев командной оболочки можно встретить двоеточие, применяемое в начале строки для обозначения комментариев, однако в современных сценариях следует всегда применять для обозначения начала комментариев знак #
, поскольку этот вариант действует эффективнее.
#!/bin/sh
rm -f fred
if [ -f fred ]; then
:
else
echo file fred did not exist
fi
exit 0
Как и одноименный оператор языка С, эта команда заставляет охватывающий ее цикл for
, while
или until
начать новый проход или следующую итерацию. При этом переменная цикла принимает следующее значение в списке.
#!/bin/sh
rm -rf fred*
echo > fred1
echo > fred2
mkdir fred3
echo > fred4
for file in fred*
do
if [ -d "$file" ]; then
echo "skipping directory $file"
continue
fi
echo file is $file
done
rm -rf fred*
exit 0
Команда continue
может принимать в качестве необязательного параметра номер прохода охватывающего цикла, с которого следует возобновить выполнение цикла.
Таким образом, вы сможете иногда выскочить из вложенных циклов. Данный параметр редко применяется, т.к. он часто сильно затрудняет понимание сценариев. Например,
for x in 1 2 3
do
echo before $x
continue 1
echo after $x
done
У приведенного фрагмента будет следующий вывод:
before 1
before 2
before 3
Команда "точка" (.
) выполняет команду в текущей оболочке:
. ./shell_script
Обычно, когда сценарий выполняет внешнюю команду или сценарий, создается новое окружение (подоболочка), команда выполняется в новом окружении и затем окружение удаляется, за исключением кода завершения, который возвращается в родительскую оболочку. Внешняя команда source и команда "точка" (еще два синонима) выполняют команды, приведенные в сценарии, в той же командной оболочке, которая выполняет сценарий.
Поскольку по умолчанию во время работы сценария создается новое окружение, любые изменения переменных окружения, сделанные в сценарии, теряются. С другой стороны, команда "точка" позволяет выполняющемуся сценарию изменять текущее окружение. Это часто бывает полезно, когда сценарий применяется как оболочка для настройки окружения, предназначенного для последующего выполнения какой-либо другой команды. Например, когда вы работаете над несколькими разными проектами одновременно, может оказаться, что вам необходимо выполнять команды с разными параметрами, например, запускать более старую версию компилятора для поддержки старой программы.
В сценариях командной оболочки команда "точка" играет роль, немного похожую на роль директивы #include в языках программирования С и С++. И хотя она не подключает сценарий в буквальном смысле слова, она действительно выполняет команду в текущем контексте, поэтому вы можете применять ее для включения переменных и определений функций в ваш сценарий.
Выполните упражнение 2.13.
В следующем примере команда "точка" применяется в командной строке, но с таким же успехом вы можете использовать ее и в сценарии.
1. Предположим, что у вас есть два файла, содержащие параметры окружения для двух разных сред разработки. Для установки окружения, предназначенного для старых классических команд, classic_set, можно применить следующий программный код.
#!/bin/sh
version=classic
PATH=/usr/local/old_bin:/usr/bin:/bin:
.
PS1="classic> "
2. Для новых команд применяется latest_set.
#!/bin/sh
version=latest
PATH=/usr/local/new_bin:/usr/bin:/bin:
.
PS1=" latest version> "
Вы можете установить окружение, применяя эти сценарии в сочетании с командой "точка", как показано в следующей порции примера.
$ . ./classic_set
classic> echo $version
classic
classic> . /latest_set
latest version> echo $version
latest
latest version>
Как это работает
Сценарии выполняются, используя команду "точка", поэтому каждый из них выполняется в текущей командной оболочке. Это позволяет сценарию изменять параметры окружения в текущей оболочке, которая сохраняет изменения даже после того, как сценарий завершился.
Несмотря на призыв группы Х/Open применять в современных командных оболочках команду printf
, мы будем продолжать следовать общепринятой практике использования команды echo
для вывода строки с последующим переходом на новую строку.
При этом возникает общая проблема: удаление символа перехода на новую строку. К сожалению, в разных версиях ОС UNIX реализованы разные решения. В ОС Linux общепринятый метод
echo -n "string to output"
Но вы часто будете сталкиваться и с вариантом
echo -е "string to output\c"
Второй вариант echo -е
рассчитан на то, что задействована интерпретация символов escape-последовательности, начинающихся с обратного слэша, таких как \c
для подавления новой строки, \t
для вывода табуляции, \n
для вывода символов возврата каретки. В более старых версиях bash этот режим установлен по умолчанию, а в более современных версиях интерпретация символов escape-последовательностей с обратным слэшем отключена. Подробные сведения о поведении вашего дистрибутива ищите на страницах интерактивного справочного руководства.
Если вам нужен легко переносимый способ удаления завершающей новой строки, для избавления от нее можно воспользоваться внешней командой tr
, но она будет выполняться немного медленнее. Если вашим системам UNIX нужна переносимость и нужно избавиться от завершающей новой строки, как правило, лучше придерживаться команды printf
. Если ваши сценарии предназначены для работы только в ОС Linux и bash, вполне подойдет echo -n
, хотя, возможно, придется начинать файл со строки #!/bin/bash
для того, чтобы в явной форме показать, что вы рассчитываете на поведение в стиле bash.
Команда eval позволяет вычислять аргументы. Она встроена в командную оболочку и обычно не представлена как отдельная команда. Лучше всего ее действие демонстрирует короткий пример, позаимствованный непосредственно из стандарта X/Open.
foo=10
x=foo
у='$'$х
echo $у
Будет выведено $foo
. Однако код
foo=10
x=foo
eval у='$'$х
echo $у
выведет на экран 10. Таким образом, eval
немного похожа на дополнительный знак $
: она возвращает значение значения переменной.
Команда eval
очень полезна, т.к. позволяет генерировать и выполнять код на лету. Применение этой команды усложняет отладку сценария, но разрешает делать то, что в противном случае выполнить сложно или даже невозможно.
У команды exec
два варианта применения. Обычно ее используют для замены текущей командной оболочки другой программой.
Например, строка
exec wall "Thanks for all the fish"
в сценарии заменит текущую оболочку командой wall
. Строки, следующие за командой exec
, не обрабатываются, потому что командная оболочка, выполнявшая сценарий, больше не существует.
Второй вариант применения exec
— модификация текущих дескрипторов файлов.
exec 3< afile
Эта команда открывает файловый дескриптор 3 для чтения из файла afile. Этот вариант редко используется.
Команда exit
вызывает завершение сценария с кодом завершения n
. Если вы примените ее в строке подсказки или приглашения любой интерактивной командной оболочки, она приведет к вашему выходу из системы. Если разрешить сценарию завершиться без указания кода завершения, статус последней выполненной в сценарии команды используется как возвращаемое значение. Задание кода завершения считается хорошим стилем программирования.
При программировании сценариев в командной оболочке код завершения 0 — успешное завершение сценария, коды от 1 до 125 включительно — коды ошибок, которые можно использовать в сценариях. Оставшиеся значения зарезервированы в соответствии с табл. 2.5.
Таблица 2.5
Код завершения | Описание |
---|---|
126 | Файл не является исполняемым |
127 | Команда не найдена |
128 и выше | Появившийся сигнал |
Многим программистам на языках С и С++ использование нуля как признака успешного завершения может показаться несколько необычным. Большое преимущество сценариев — возможность применения 125 кодов ошибок, определенных пользователем, и отсутствие необходимости в глобальной переменной для хранения кода ошибки.
Далее приведен простой пример, возвращающий код успешного завершения, если в текущем каталоге существует файл с именем .profile.
#!/bin/sh
if [ -f .profile ]; then
exit 0
fi
exit 1
Если вы любитель острых ощущений или, как минимум, лаконичных сценариев, можете переписать сценарий в виде одной строки, используя комбинацию И-списка и ИЛИ-списка, описанных ранее:
[ -f .profile ] && exit 0 || exit 1
Команда export
делает переменную, называемую ее параметром, доступной в подоболочках. По умолчанию переменные, созданные в командной оболочке, не доступны в новых дочерних подоболочках, запускаемых из данной. Команда export
создает из своего параметра переменную окружения, которая видна другим сценариям и программам, запускаемым из текущей программы. Говоря профессиональным языком, экспортируемые переменные формируют переменные окружения в любых дочерних процессах, порожденных командной оболочкой. Лучше всего проиллюстрировать это примером из двух сценариев: export1
и export2
(упражнение 2.14).
1. Первым представим сценарий export2.
#!/bin/sh
echo "$foo"
echo "$bar"
2. Теперь сценарий export1. В конце сценария запускается export2.
#!/bin/sh
foo="The first meta-syntactic variable"
export bar="The second meta-syntactic variable"
export2
Если вы запустите их, то получите следующий результат.
$ ./export1
The second meta-syntactic variable
$
Как это работает
Сценарий export2 просто выводит значения двух переменных. В сценарии export1 задаются значения обеих переменных, но только переменная bar помечается как экспортируемая, поэтому, когда впоследствии запускается сценарий export2, значение переменной foo
потеряно, а значение переменной bar
экспортировано во второй сценарий. На экране появляется пустая строка, поскольку $foo
ничего не содержит и вывод переменной со значением null
приводит к отображению новой строки.
После того как переменная была экспортирована из командной оболочки, она экспортируется в любые сценарии, запускаемые из этой оболочки, и в любые командные оболочки, которые в свою очередь запускают эти сценарии, и т.д. Если бы сценарий export2 вызвал другой сценарий, в нем переменная bar
также была бы доступна.
Команды set -а
или set -allexport
экспортируют все переменные соответственно.
Команда expr
вычисляет выражение, составленное из ее аргументов. Чаще всего она применяется для подсчета простых арифметических выражений в следующем виде:
х=`expr $x + 1`
Символы ``
(обратная кавычка или обратный апостроф) заставляют переменную х
принять результат выполнения команды expr $х + 1
. Ее можно также записать с помощью синтаксической конструкции $( )
вместо обратной кавычки, например, следующим образом:
х=$(expr $х + 1)
Команда expr обладает большими возможностями, с ее помощью можно вычислять различные выражения. Основные виды вычислений перечислены в табл. 2.6.
Таблица 2.6
Вычисление выражения | Описания |
---|---|
Выражение1 | Выражение2 | Выражение1 , еслиВыражение1 не равно нулю, в противном случаеВыражение2 |
Выражение1 & Выражение2 | Нуль, если оба выражения равны нулю, в противном случаеВыражение1 |
Выражение1 = Выражение2 | Равенство |
Выражение1 > Выражение2 | Больше чем |
Выражение1 >= Выражение2 | Больше или равно |
Выражение1 < Выражение2 | Меньше чем |
Выражение1 <= Выражение2 | Меньше или равно |
Выражение1 != Выражение2 | Неравенство |
Выражение1 + Выражение2 | Сложение |
Выражение1 — Выражение2 | Вычитание |
Выражение1 * Выражение2 | Умножение |
Выражение1 / Выражение2 | Деление нацело |
Выражение1 % Выражение2 | Остаток от деления нацело |
В современных сценариях вместо команды expr
обычно применяется более эффективная синтаксическая конструкция $((...))
, которая будет описана далее в этой главе.
Команда printf
есть только в современных командных оболочках. Группа X/Open полагает, что ее следует применять вместо команды echo
для генерации форматированного вывода, несмотря на то, что, кажется, лишь немногие следуют этому совету.
У команды следующая синтаксическая запись.
printf "строка формата" параметр1 параметр2 ...
Строка формата очень похожа с некоторыми ограничениями на применяемую в языках программирования С и С++. Главным образом не поддерживаются числа с плавающей точкой, поскольку все арифметические операции в командной оболочке выполняются над целыми числами. Строка формата состоит из произвольной комбинации литеральных символов, escape-последовательностей и спецификаторов преобразования. Все символы строки формата, отличающиеся от \
и %
, отображаются на экране при выводе.
В табл. 2.7 приведены поддерживаемые командой escape-последовательности.
Таблица 2.7
Escape-последовательность | Описание |
---|---|
\" | Двойная кавычка |
\\ | Символ обратный слэш |
\a | Звуковой сигнал тревоги (звонок колокольчика или прерывистый звуковой сигнал) |
\b | Символ Backspace (стирание слева) |
\c | Отбрасывание последующего вывода |
\f | Символ Form feed (подача бумаги) |
\n | Символ перехода на новую строку |
\r | Возврат каретки |
\t | Символ табуляции |
\v | Символ вертикальной табуляции |
\ooo | Один символ с восьмеричным значением ooo |
\xHH | Один символ с шестнадцатеричным значением HH |
Спецификаторы преобразований довольно сложны, поэтому мы приведем наиболее распространенные варианты их применения. Более подробную информацию можно найти в интерактивном справочном руководстве командной оболочки bash или на страницах раздела 1 интерактивного руководства к команде printf
(man 1 printf
). (Если вы не найдете нужных сведений в разделе 1, попробуйте поискать в разделе 3.) Спецификатор преобразования состоит из символа %
, за которым следует символ преобразования. Основные варианты преобразований перечислены в табл. 2.8.
Таблица 2.8
Символ преобразования | Описание |
---|---|
D | Вывод десятичного числа |
С | Вывод символа |
S | Вывод строки |
% | Вывод знака % |
Строка формата используется для интерпретации остальных параметров команды и вывода результата, как показано в следующем примере:
$ printf "%s\n" hello
hello
$ printf "%s %d\t%s" "Hi There" 15 people
Hi There 15 people
Обратите внимание на то, что для защиты строки Hi There
и превращения ее в единый параметр, строку нужно заключить в кавычки (""
).
Команда return
служит для возврата значений из функций, как уже упоминалось ранее при обсуждении функций. Команда принимает один числовой параметр, который становится доступен в сценарии, вызывающем функцию. Если параметр не задан, команда return
по умолчанию возвращает код завершения последней команды.
Команда set
задает переменные-параметры командной оболочки. Она может быть полезна при использовании полей в командах, выводящих значения, разделенные пробелами.
Предположим, что вы хотите использовать в сценарии название текущего месяца. В системе есть команда date
, содержащая название месяца в виде строки, но нужно отделить его от других полей. Это можно сделать с помощью комбинации команды set
и конструкции $(...)
, которые обеспечат выполнение команды date
и возврат результата (более подробно об этом см. далее). В выводе команды date строка с названием месяца — второй параметр.
#!/bin/sh
echo the date is $(date)
set $(date)
echo The month is $2
exit 0
Программа задает список параметров для вывода команды date
и затем использует позиционный параметр $2
для получения названия месяца.
Мы использовали команду date
только как простой пример, демонстрирующий, как извлекать позиционные параметры. Поскольку команда date
зависит от языковых параметров или локализации, в действительности мы бы извлекли название месяца командой date +%B
. У команды date
много других вариантов форматирования, более подробную информацию см. на страницах интерактивного справочного руководства к команде.
Команду set
можно также применять для передачи параметров командной оболочке и тем самым управления режимом ее работы. Наиболее часто используемый вариант команды set -х
, который заставляет сценарий выводить на экран трассировку выполняемой в данный момент команды. Мы обсудим команду set
и ее дополнительные опции позже в этой главе, когда будем рассматривать отладку программ.
Команда shift
сдвигает все переменные-параметры на одну позицию назад, так что параметр $2
становится параметром $1
, параметр $3
— $2
и т.д. Предыдущее значение параметра $1
отбрасывается, а значение параметра $0
остается неизменным. Если в вызове команды shift
задан числовой параметр, параметры сдвигаются на указанное количество позиций. Остальные переменные $*
, $@
и $#
также изменяются в связи с новой расстановкой переменных-параметров.
Команда shift
часто полезна при поочередном просмотре параметров, переданных в сценарий, и если вашему сценарию требуется 10 и более параметров, вам понадобится команда shift
для обращения к 10-му параметру и следующим за ним.
Например, вы можете просмотреть все позиционные параметры:
#!/bin/sh
while [ "$1" != "" ]; do
echo "$1"
shift
done
exit 0
Команда trap
применяется для задания действий, предпринимаемых при получении сигналов, которые подробно будут обсуждаться далее в этой книге. Обычное действие — удалить сценарий, когда он прерван. Исторически командные оболочки всегда использовали числа для обозначения сигналов, но в современных сценариях следует применять имена, которые берутся из файла signal.h директивы #include
с опущенным префиксом SIG
. Для того чтобы посмотреть номера сигналов и соответствующие им имена, можно ввести в командной строке команду trap -l
.
Для тех, кто не знаком с сигналами, это события, асинхронно посылаемые программе. Стандартно они обычно вызывают прекращение выполнения программы.
С помощью команды trap
передается предпринимаемое действие, за которым следует имя (имена) сигнала для перехвата:
trap команда сигнал
Напоминаем, что обычно сценарии обрабатываются интерпретатором сверху вниз, поэтому вы должны задать, команду trap
перед той частью сценария, которую хотите защитить.
Для возврата к стандартной реакции на сигнал, просто задайтекоманду
как -
. Для игнорирования сигнала задайте вкоманде
пустую строку ''
. Команда trap без параметров выводит текущий список перехватов и действий.
В табл. 2.9 перечислены самые важные, включенные в. стандарт Х/Open сигналы, которые можно отследить (со стандартными номерами в скобках). Дополнительную информацию можно найти на страницах раздела 7 интерактивного справочного руководства, посвященного сигналам (man 7 signal
).
Таблица 2.9
Сигнал | Описание |
---|---|
HUP (1) | Неожиданный останов; обычно посылается, когда отключается терминал или пользователь выходит из системы |
INT (2) | Прерывание; обычно посылается нажатием комбинации клавиш <Ctrl>+<C> |
QUIT (3) | Завершение выполнения; обычно посылается нажатием комбинации клавиш <Ctrl>+<\> |
ABRT (6) | Аварийное завершение; обычно посылается при возникновении серьезной ошибки выполнения |
ALRM (14) | Аварийный сигнал; обычно посылается для обработки превышений лимита времени |
TERM (15) | Завершение; обычно посылается системой, когда она завершает работу |
А теперь выполните упражнение 2.15.
В следующем сценарии показана простая обработка сигнала.
#!/bin/sh
trap 'rm -f /tmp/my_tmp_file_$$' INT
echo creating file /tmp/my_tmp_file_$$
date > /tmp/my_tmp_file_$$
echo "press interrupt (CTRL-C) to interrupt..."
while [ -f /tmp/my_tmp_file_$$ ] ; do
echo File exists
sleep 1
done
echo The file no longer exists trap INT
echo creating file /tmp/my_tmp_file_$$
date > /tmp/my_tmp_file_$$
echo "press interrupt (CTRL-C) to interrupt..."
while [ -f /tmp/my_tmp_file_$$ ]; do
echo File exists
sleep 1
done
echo we never get here
exit 0
Если вы выполните этот сценарий, нажимая и удерживая нажатой клавишу <Ctrl> и затем нажимая клавишу <C> (или любую другую прерывающую комбинацию клавиш) в каждом из циклов, то получите следующий вывод:
creating file /tmp/my_tmp_file_141
press interrupt (CTRL-C) to interrupt ...
File exists
File exists
File exists
File exists
The file no longer exists
creating file /tmp/my tmp_file_141
press interrupt (CTRL-C) to interrupt ...
File exists
File exists
File exists
File exists
Как это работает
Сценарий использует команду trap
для организации выполнения команды rm -f /tmp/my_tmp_file_$$
при возникновении сигнала INT
(прерывание). Затем сценарий выполняет цикл while
до тех пор, пока существует файл. Когда пользователь нажимает комбинацию клавиш <Ctrl>+<C>, выполняется команда rm -f /tmp/my_tmp_file_$$
, а затем возобновляется выполнение цикла while
. Поскольку теперь файл удален, первый цикл while
завершается стандартным образом.
Далее сценарий снова применяет команду trap
, на этот раз для того, чтобы сообщить, что при возникновении сигнала INT
никакая команда не выполняется. Затем сценарий создает заново файл и выполняет второй цикл while
. Когда пользователь снова нажимает комбинацию клавиш <Ctrl>+<C>, не задана команда для выполнения, поэтому реализуется стандартное поведение: немедленное прекращение выполнения сценария. Поскольку сценарий завершается немедленно, заключительные команды echo
и exit
никогда не выполняются.
Команда unset
удаляет переменные или функции из окружения. Она не может проделать это с переменными, предназначенными только для чтения и определенными командной оболочкой, такими как IFS
. Команда применяется редко.
В следующем сценарии сначала выводится строка Hello world
, а во второй раз новая строка.
#!/bin/sh
foo="Hello World"
echo $foo
unset foo
echo $foo
Написание foo=
подобно, но не идентично применению команды unset
в только что приведенной программе. Оператор foo=
задает для переменной foo
значение null
, но при этом переменная foo
все еще существует. Команда unset foo
удаляет из окружения переменную foo
.
Прежде чем вы увидите, как применять на практике полученные навыки программирования в командной оболочке, давайте рассмотрим еще две очень полезные команды, которые, хотя и не являются часть оболочки, очень пригодятся при написании ее программ. Попутно мы также познакомимся с регулярными выражениями, средством поиска по шаблону, которое обнаруживает себя в разных частях ОС Linux и тесно связанных с нею программах.
Первой рассмотрим команду find
. Эта команда, применяющаяся для поиска файлов, чрезвычайно полезна, но новички в ОС Linux часто находят ее немного сложной в использовании в немалой степени из-за ее опций, критериев и аргументов, определяющих действия (action-type), причем результат одного из этих аргументов может влиять на обработку последующих аргументов.
Прежде чем углубиться в изучение опций, критериев и аргументов команды, рассмотрим очень простой пример поиска на вашей машине файла test
. Выполните приведенную далее команду под именем суперпользователя root, чтобы иметь достаточно прав доступа для обследования всего компьютера.
# find / -name test -print
/usr/bin/test
#
В зависимости от варианта установки системы на вашей машине вы можете найти и другие файлы, также названные test. Как вы, вероятно, догадываетесь, команда звучит так: "искать, начиная с каталога /, файл с именем test и затем вывести на экран имя файла". Легко, не правда ли? Безусловно.
Выполнение команды займет какое-то время, она будет искать на нашей машине и на сетевом диске машины с ОС Windows. Это происходит потому, что на компьютере с Linux смонтирована (с помощью пакета SAMBA) порция файловой системы машины с ОС Windows. Похоже, что подобный поиск будет вестись, даже если мы знаем, что искомый файл находится на машине под управлением ОС Linux.
В этом случае на помощь приходит первая опция. Если вы укажете опцию -mount
, то сможете сообщить команде find о том, что смонтированные каталоги проверять не нужно.
# find / -mount -name test -print
/usr/bin/test
#
Мы нашли все тот же файл на нашей машине, но на сей раз гораздо быстрее и без поиска в смонтированных файловых системах.
Полная синтаксическая запись команды find
выглядит следующим образом:
find [путь] [опции] [критерии] [действия]
Часть записи [путь]
понятна и проста: вы можете указать абсолютный путь поиска, например, /bin
, или относительный, например ..
При необходимости можно задать несколько путей — например, find /var /home
.
В табл. 2.10 перечислены основные опции команды.
Таблица 2.10
Опция | Описание |
---|---|
-depth | Поиск в подкаталогах перед поиском в самом каталоге |
-follow | Следовать по символическим ссылкам |
-maxdepths N | При поиске проверять не болееN вложенных уровней каталога |
-mount (или -xdev ) | Не искать в каталогах других файловых систем |
Теперь о критериях. В команде find
можно задать большое число критериев, и каждый из них возвращает либо true
, либо false
. В процессе работы команда find
рассматривает по очереди каждый файл и применяет к нему все критерий в порядке их определения. Если очередной критерий возвращает значение false
, команда find
прекращает анализ текущего файла и переходит к следующему; если критерий возвращает значение true
, команда применяет следующий критерий к текущему файлу или совершает заданное действие над ним. В табл. 2.11 перечислены самые распространенные критерии; полный список тестов, которые можно применять в команде find
, вы найдете на страницах интерактивного справочного руководства.
Таблица 2.11
Критерий | Описание |
---|---|
-atime N | К файлу обращались последний раз N дней назад |
-mtime N | Файл последний раз изменялся N дней назад |
-name шаблон | Имя файла без указания пути соответствует заданному шаблону. Для гарантии того, что шаблон будет передан в команду find и не будет немедленно обработан командной оболочкой, его следует всегда заключать в кавычки |
-newer другой файл | Текущий файл, измененный позже, чем другой файл |
-type С | Файл типа C , где C может принимать определенные значения; наиболее широко используемые "d " для каталогов и "f " для обычных файлов. Остальные обозначения типов можно посмотреть на страницах интерактивного справочного руководства |
-user имя пользователя | Файл принадлежит пользователю с заданным именем |
Вы также можете объединять критерии с помощью операторов. Как показано в табл. 2.12, у большинства из них две формы записи: короткая и более длинная форма.
Таблица 2.12
Оператор, короткая форма | Оператор, длинная форма | Описание |
---|---|---|
! | -not | Инвертирование критерия |
-а | -and | Оба критерия должны быть истинны |
-о | -or | Один из критериев должен быть истинным |
Изменить порядок проверки критериев и выполнения операторов можно с помощью скобок. Поскольку в командной оболочке у них есть особое назначение, скобки также следует выделять с помощью обратного слэша. Кроме того, если вы применяете шаблон для имени файла, то следует использовать кавычки, чтобы оболочка не выполняла подстановку имени, а прямо передала шаблон команде find
. Например, если вы хотите задать критерий "измененный позже, чем файл X
, или с именем, начинающимся со знака подчеркивания", его можно записать следующим образом:
\(-newer X -о -name "_*" \)
Мы приведем пример сразу после описания "Как это работает". А сейчас выполните упражнение 2.16.
Попытаемся найти в текущем каталоге файлы, измененные после модификации файла while2.
$ find . -newer while2 -print
.
./elif3
./words.txt
./words2.txt
./_trap
$
Все чудесно, за исключением того, что вы нашли ненужный вам текущий каталог. Вас интересуют только обычные файлы, поэтому добавьте дополнительный критерий -type f
.
$ find . -newer while2 -type f -print
./elif3
./words.txt
./words2.txt
./_trap
$
Как это работает
Как это работает? Вы определили, что команда find
должна искать в текущем каталоге (.
) файлы, измененные позже, чем файл while2 (-newer while2
), и, если этот критерий пройден, проверять с помощью следующего критерия (-type f
), обычные ли это файлы. В заключение вы применили действие, с которым уже сталкивались, -print
, просто для того чтобы подтвердить, что файлы были найдены.
Теперь найдем файлы с именами, начинающимися с символа подчеркивания или измененные позже, чем файл while2, но в любом случае обычные файлы. Этот пример покажет, как объединять критерии с помощью скобок.
$ find . \( -name "_*" -or -newer while2 \) -type f -print
./elif3
./words.txt
./words2.txt
./_break
./_if
./set
./_shift
./_trap
./_unset
./ until
$
Это не слишком трудный пример, не так ли? Вы должны экранировать скобки, чтобы они не обрабатывались командной оболочкой, и заключить в кавычки символ *
, чтобы он также был передан непосредственно в команду find
.
Теперь, когда вы можете правильно искать файлы, рассмотрим действия, которые можно совершить, когда найден файл, соответствующий вашей спецификации. И снова в табл. 2.13 перечислены только самые популярные действия; полный список можно найти на страницах интерактивного справочного руководства.
Таблица 2.13
Действие | Описание |
---|---|
-exec команда | Выполняеткоманду . Наиболее широко используемое действие. После табл. 2.13 приведено объяснение способа передачи параметров в команду. Это действие следует завершать символьной парой \; |
-ok команда | Подобно действию exec , за исключением того, что перед обработкой файловкомандой выводится подсказка для получения подтверждения пользователя на обработку каждого файла. Это действие следует завершать символьной парой \; |
-print | Вывод на экран имени файла |
-ls | Применение команды ls -dils к текущему файлу |
Команда в аргументах -exec
и -ok
принимает последующие параметры в строке как собственные, пока не встретится последовательность \;
В действительности команда, в аргументах -exec
и -ok
выполняет встроенную команду, поэтому встроенная команда должна завершиться экранированной точкой с запятой, для того чтобы команда find
могла определить, когда ей следует продолжить поиск в командной строке аргументов, предназначенных для нее самой. Магическая строка {}
— параметр специального типа для команд -exec
и -ok
, который заменяется полным путем к текущему файлу.
Объяснение, возможно, не слишком легкое для понимания, поэтому рассмотрим пример, который поможет внести ясность. Взгляните на простой пример, использующий хорошую безопасную команду ls
:
$ find . -newer while2 -type f -exec ls -l {} \;
-rwxr-xr-x 1 rick rick 275 Feb 8 17:07 ./elif3
-rwxr-xr-x 1 rick rick 336 Feb 8 16:52 ./words.txt
-rwxr-xr-x 1 rick rick 1274 Feb 8 16:52 ./words2.txt
-rwxr-xr-x 1 rick rick 504 Feb 8 18:43 ./_trap
$
Как видите, команда find
чрезвычайно полезна; она только требует небольшой практики для умелого ее применения. И такая практика, как и эксперименты с командой find
, обязательно принесет дивиденды.
Вторая очень полезная команда, заслуживающая рассмотрения, — это команда grep
. Необычное имя, означающее общий синтаксический анализатор регулярных выражений (general regular expression parser). Вы применяете команду find
для поиска файлов в вашей системе, а команду grep
для поиска строк в ваших файлах. Действительно, очень часто при использовании команды find
команда grep
передается после аргумента -exec
.
Команда grep
принимает опции, шаблон соответствия и файлы для поиска:
grep [опции] шаблон [файлы]
Если имена файлов не заданы, команда анализирует стандартный ввод.
Давайте начнем с изучения основных опций команды grep
. И на этот раз в табл. 2.14 приведены только самые важные из них; полный список см. на страницах интерактивного справочного руководства.
Таблица 2.14
Опция | Описание |
---|---|
-с | Вместо вывода на экран совпавших с шаблоном строк выводит их количество |
-E | Включает расширенные регулярные выражения |
-h | Ужимает обычное начало каждой строки вывода за счет удаления имени файла, в котором строка найдена |
-i | Не учитывает регистр букв |
-l | Перечисляет имена файлов со строками, совпадающими с шаблоном; не выводит сами найденные строки |
-v | Меняет шаблон соответствия для выбора вместо строк, соответствующих шаблону, несовпадающих с ним строк |
Выполните упражнение 2.17.
Посмотрим команду grep
в действии на примерах простых шаблонов.
$ grep in words.txt
When shall we three meet again. In thunder, lightning, or in rain?
I come, Graymalkin!
$ grep -c in words.txt words2.txt
words.txt:2 words2.txt:14
$ grep -c -v in words.txt words2.txt
words.txt:9
words2.txt:16$
Как это работает
В первом примере нет опций; в нем просто ищется строка in
в файле words.txt и выводятся на экран любые строки, соответствующие условию поиска. Имя файла не отображается, поскольку поиск велся в единственном файле.
Во втором примере в двух разных файлах подсчитывается количество строк, соответствующих шаблону. В этом случае имена файлов выводятся на экран.
В заключение применяется опция -v
для инвертирования критерия поиска и подсчета строк, не совпадающих с шаблоном.
Как вы убедились, основной вариант применения команды grep легко усвоить. Теперь пришло время рассмотреть основы построения регулярных выражений, которые позволят вам выполнять более сложный поиск. Как упоминалось ранее в этой главе, регулярные выражения применяются в системе Linux и многих языках программирования с открытым исходным кодом. Вы можете использовать их в редакторе vi и в скриптах на языке Perl, применяя одни и те же принципы, общие для регулярных выражений, где бы они ни появлялись.
При обработке регулярных выражений определенные символы интерпретируются особым образом. В табл. 2.15 приведены наиболее часто используемые в регулярных выражениях символы.
Таблица 2.15
Символ | Описание |
---|---|
^ | Привязка к началу строки |
$ | Привязка к концу строки |
. | Любой одиночный символ |
[] | В квадратных скобках содержится диапазон символов, с любым из них возможно совпадение, например, диапазон символов a-e или инвертированный диапазон, перед которым стоит символ ^ |
Если вы хотите использовать любые из перечисленных символов как "обычные", поставьте перед ними символ \
. Например, если нужно найти символ $
, просто введите \$
.
Есть также несколько полезных специальных проверочных шаблонов, которые могут указываться в квадратных скобках (табл. 2.16).
Таблица 2.16
Проверочный шаблон | Описание |
---|---|
[:alnum:] | Буквенно-цифровые символы |
[:alpha:] | Буквы |
[:ascii:] | Символы таблицы ASCII |
[:blank:] | Пробел или табуляция |
[:cntrl:] | Управляющие символы ASCII |
[:digit:] | Цифры |
[:graph:] | Неуправляющие и непробельные символы |
[:lower:] | Строчные буквы |
[:print:] | Печатные символы |
[:punct:] | Знаки пунктуации |
[:space:] | Пробельные символы, включая вертикальную табуляцию |
[:upper:] | Прописные буквы |
[:xdigit:] | Шестнадцатиричные цифры |
Кроме того, если задана опция =E
для расширенного соответствия, за регулярным выражением могут следовать и другие символы, управляющие выполнением проверки на соответствие шаблону (табл. 2.17). В команде grep
перед этими символами необходимо вводить символ \
.
Таблица 2.17
Опция | Описание |
---|---|
? | Совпадение не обязательно, но возможно не более одного раза |
* | Совпадения может не быть, оно может быть однократным или многократным |
+ | Совпадение должно быть однократным или многократным |
{n} | Совпадение должно быть n раз |
{n, } | Совпадение должно быть n раз и больше |
{n, m} | Совпадение должно быть от n до m раз включительно |
Все это выглядит немного запутанно, но если осваивать все возможности постепенно, то вы увидите, что все не так сложно, как кажется на первый взгляд. Самый легкий способ понять регулярные выражения — просто попробовать применить несколько.
1. Начнем с поиска строк, заканчивающихся буквой "е". Возможно, вы уже догадались, что нужно использовать специальный символ $
:
$ grep e$ words2.txt
Art thou not, fatal vision, sensible
I see thee yet, in form as palpable
Nature seems dead, and wicked dreams abuse
$
Как видите, найдены строки, заканчивающиеся буквой "е".
2. Теперь найдите трехбуквенные слова, начинающиеся с символов "Th". В данном случае вам понадобится шаблон [[:space:]]
для ограничения длины слова и .
для единственного дополнительного символа.
$ grep Th.[[:space:]] words 2.txt
The handle toward my hand? Come, let me clutch thee.
The curtain'd sleep; witchcraft celebrates
Thy very stones prate of my whereabout,
$
3. В заключение примените расширенный режим поиска в команде grep
для обнаружения слов из строчных букв длиной ровно 10 символов. Для этого задайте диапазон совпадающих символов от а до z и 10 повторяющихся совпадений.
$ grep -Е [a-z]\{10\} words2.txt
Proceeding from the heat-oppressed brain?
And such an instrument I was to use.
The curtain'd sleep; witchcraft celebrates
hy very stones prate of my whereabout,
$
Приведенные примеры лишь коснулись наиболее важных компонентов регулярных выражений. Как и для большинства составных частей ОС Linux, существует множество дополнительной документации помимо этой книги, которая поможет вам узнать еще больше подробностей, но лучший способ изучения регулярных выражений — экспериментировать с ними.
При написании сценариев вам часто требуется перехватить результат выполнения команды для использования его в сценарии командной оболочки; т.е. вы хотите выполнить команду и поместить ее вывод в переменную.
Сделать это можно с помощью синтаксической конструкции $(команда)
, показанной ранее в примере с командой set
. Существует устаревший вариант подстановки команды `команда`
, который все еще широко распространен.
В более раннем варианте конструкции применяется обратный апостроф или обратная кавычка (`
), а не обычный апостроф ('
), который мы использовали раньше в командной оболочке для экранирования (защиты от подстановки переменных). В сценариях оболочки применяйте этот вариант, только если вы хотите добиться высокой степени переносимости сценариев.
Во всех современных сценариях следует применять конструкцию выполнения или подстановки команды $(команда)
, которая введена для того, чтобы избавиться от довольно сложных правил использования символов $
, '
и \
внутри команды, заключенной в обратные апострофы. Если применяется обратный апостроф внутри конструкции `...`
, его необходимо экранировать символом \
. Эти непонятные знаки часто заставляют программистов путаться, и иногда даже опытные специалисты в программировании средствами командной оболочки вынуждены ставить опыты для того, чтобы добиться правильного использования кавычек и апострофов в командах, заключенных в обратные апострофы.
Результат выполнения конструкции $(команда)
— просто вывод команды. Имейте в виду, что это не статус возврата команды, а просто строковый вывод, показанный далее.
#!/bin/sh
echo The current directory is $PWD
echo The current users are $(who)
exit 0
Поскольку текущий каталог — это переменная окружения командной оболочки, первая строка не нуждается в применении подстановки команды. Результат выполнения программы who
, напротив, нуждается в ней, если он должен стать переменной в сценарии.
Если вы хотите поместить результат в переменную, то можете просто присвоить его обычным образом:
whoisthere=$(who)
echo Swhoisthere
Возможность поместить результат выполнения команды в переменную сценария — очень мощное средство, поскольку оно облегчает использование существующих команд в сценариях и перехват результата их выполнения. Если когда-нибудь вам понадобится преобразовать набор параметров, представляющих собой вывод команды на стандартное устройство вывода, и передать их как аргументы в программу, возможно, вас порадует то, что команда xargs
сможет это сделать за вас. Дополнительные подробности ищите на страницах интерактивного справочного руководства.
Иногда возникают проблемы, если команда, которую вы хотите выполнить, выводит несколько пробелов перед нужным вам текстом, или больше информации, чем вам нужно. В таких случаях можно воспользоваться командой set
, как было показано ранее.
Мы уже использовали команду expr
, которая позволяет выполнять простые арифметические операции, но она делает это очень медленно, потому что для выполнения команды expr
запускается новая командная оболочка.
Современная и лучшая альтернатива — синтаксическая конструкция $((...))
. Поместив в эту конструкцию выражение, которое вы хотите вычислить, вы можете выполнить простые арифметические операции гораздо эффективнее.
#!/bin/sh
х=0
while [ "$х" -ne 10 ]; do
echo $х
х=$(($x+1))
done
exit 0
Обратите внимание на тонкое отличие приведенной подстановки от команды х=$(...)
. Двойные скобки применяются для подстановки значений в арифметические выражения. Вариант с одиночными скобками, показанный ранее, используется для выполнения команд и перехвата их вывода.
Вы уже видели простейший вариант присваивания параметра и подстановки значения параметра:
foo=fredecho $foo
Проблема возникает, когда вы хотите вставить дополнительные символы в конец значения переменной. Предположим, что вы хотите написать короткий сценарий обработки файлов 1_tmp и 2_tmp. Вы могли бы написать следующие строки:
#!/bin/sh
for i in 1 2 do
my_secret_process $i_tmp
done
Но в каждом проходе цикла вы получите следующее сообщение:
my_secret_process: too few arguments
В чем ошибка?
Проблема заключается в том, что командная оболочка попыталась подставить значение переменной $i_tmp
, которая не существует. Оболочка не считает это ошибкой; она просто не делает никакой подстановки, поэтому в сценарий my_secret_process не передаются никакие параметры. Для обеспечения подстановки в переменную части ее значения $i
необходимо i
заключить в фигурные скобки следующим образом:
#!/bin/sh
for i in 1 2 do
my_secret_process ${i}_tmp
done
В каждом проходе цикла вместо ${i}
подставляется значение i
и получаются реальные имена файлов. Вы подставляете значение параметра в строку.
В командной оболочке можно выполнять разнообразные виды подстановок. Часто они помогают найти красивое решение задач, требующих обработки многих параметров. Самые распространенные виды подстановок значений параметров приведены в табл. 2.18.
Таблица 2.18
Шаблон подстановки параметра | Описание |
---|---|
${парам:-значение по умолчанию} | Если упарам нет значения, ему присваивается значение по умолчанию |
${#парам} | Задается длинапарам |
${парам%строка} | От конца значенияпарам отбрасывается наименьшая порция, совпадающая сострокой , и возвращается остальная часть значения |
${парам%%строка} | От конца значенияпарам отбрасывается наибольшая порция, совпадающая сострокой , и возвращается остальная часть значения |
${парам#строка} | От начала значенияпарам отбрасывается наименьшая порция, совпадающая сострокой , и возвращается остальная часть значения |
${парам##строка} | От начала значенияпарам отбрасывается наибольшая порция, совпадающая сострокой , и возвращается остальная часть значения |
Эти подстановки очень полезны при работе со строками. Последние четыре варианта, удаляющие части строк, особенно пригодятся при обработке имен файлов и путей к ним, как показано в упражнении 2.18.
В приведенном далее сценарии показано применение шаблонов при подстановках значений параметров.
#!/bin/sh
unset foo
echo ${foo:-bar}
foo=fud
echo ${foo:-bar}
foo=/usr/bin/X11/startx
echo ${foo#*/}
echo ${foo##*/}
bar=/usr/local/etc/local/networks
echo ${bar%local*}
echo ${bar%%local*}
exit 0
У этого сценария следующий вывод:
bar
fud
usr/bin/X11/startx
startx
/usr/local/etc/usr
Как это работает
Первая подстановка ${foo:-bar}
дает значение bar
, поскольку у foo
нет значения в момент выполнения команды. Переменная foo
остается неизменной, т.е. она остается незаданной.
Подстановка ${foo:=bar}
установила бы значение переменной $foo
. Этот строковый шаблон устанавливает, что переменная foo
существует и не равна null
. Если значение переменной не равно null
, оператор возвращает ее значение, в противном случае вместо этого переменной foo
присваивается значение bar
.
Подстановка ${foo:?bar}
выведет на экран foo: bar
и аварийно завершит команду, если переменной foo
не существует или ее значение не определено. И наконец, ${foo:+bar}
вернет bar
, если foo
существует и не равна null
. Какое разнообразие вариантов!
Шаблон {foo#*/}
задает поиск и удаление только левого символа /
(символ *
соответствует любой строке, в том числе и пустой). Шаблон {foo##*/}
задает поиск максимальной подстроки, совпадающей с ним, и, таким образом, удаляет самый правый символ / и все предшествующие ему символы.
Шаблон ${bar%local*}
определяет просмотр символов в значении параметра, начиная от крайнего правого, до первого появления подстроки local
, за которой следует любое количество символов, а в случае шаблона ${bar%%local*}
ищется максимально возможное количество символов, начиная от крайнего правого символа значения и заканчивая крайним левым появлением подстроки local
.
Поскольку в системах UNIX и Linux многое основано на идеи фильтров, результат какой-либо операции часто должен перенаправляться вручную. Допустим, вы хотите преобразовать файлы GIF в файлы JPEG с помощью программы cjpeg:
$ cjpeg image.gif > image.jpg
Порой вам может потребоваться выполнить такого рода операцию над большим числом файлов. Как автоматизировать подобное перенаправление? Это очень просто:
#!/bin/sh
for image in *.gif
do
cjpeg $image > {image%%gif}jpg
done
Этот сценарий, giftojpeg, создает в текущем каталоге для каждого файла формата GIF файл формата JPEG.
Особый способ передачи из сценария командной оболочки входных данных команде — использование встроенного документа (here document). Такой документ позволяет команде выполняться так, как будто она читает данные из файла или с клавиатуры, в то время как на самом деле она получает их из сценария.
Встроенный документ начинается со служебных символов <<
, за которыми следует специальная символьная последовательность, повторяющаяся и в конце документа. Символы <<
обозначают в командной оболочке перенаправление данных, которое в данном случае заставляет вход команды превратиться во встроенный документ. Специальная последовательность символов действует как маркер, указывая оболочке, где завершается встроенный документ. Маркерная последовательность не должна включаться в строки, передаваемые команде, поэтому лучше всего сделать ее запоминающейся и четко выделяющейся.
Рассмотрим упражнение 2.19.
Простейший пример просто передает входные данные команде cat
.
#!/bin/sh
cat <<!FUNKY!
hello
this is a here
document
!FUNKY!
Этот пример выводит на экран следующие строки:
hello
this is a here
document
Встроенные документы могут показаться странным средством, но они очень полезны, т.к. позволяют запускать интерактивные программы, например редактор, и снабжать их каким-либо заранее определенным вводом. Но особенно часто они применяются при выводе из сценария больших порций текста, как вы уже видели, и при этом позволяют избавиться от необходимости применять команду echo
для каждой выводимой строки. Вы можете использовать восклицательные знаки (!
) с двух сторон от идентификатора документа во избежание путаницы.
Если вы хотите обработать несколько строк заранее определенным способом, можно применить в сценарии строчный редактор ed и передать ему команды из встроенного документа (упражнение 2.20).
1. Начнем с файла, названного a_text_file и содержащего следующие строки:
That is line 1
That is line 2
That is line 3That is line 4
2. Вы можете отредактировать этот файл, совместно используя встроенный документ и редактор ed:
#!/bin/sh
ed a_text_file <<!FunkyStuff!
3
d
., \$s/is/was/ w
q
!FunkyStuff!
exit 0
Если вы выполните этот сценарий, то увидите, что теперь файл содержит следующие строки:
That is line 1
That is line 2
That was line 4
Как это работает
Сценарий командной оболочки запускает редактор ed и передает ему команды, необходимые для перехода к третьей строке, удаления строки и затем замены ее содержимым текущей строки (поскольку строка 3 (line 3) была удалена, теперь текущая строка — последняя строка файла). Эти команды редактора ed берутся из строк сценария, формирующих встроенный документ, строк между маркерами !Funky Stuff!
.
Обратите внимание на знак \
внутри встроенного документа, применяемый для защиты от подстановки, выполняемой командной оболочкой. Символ \
экранирует знак $
, поэтому оболочка знает, что не следует пытаться подставить вместо строки \$s/is/was/
ее значение, которого у нее конечно же нет. Оболочка просто передает текст \$
как $, который затем сможет интерпретировать редактор e
Обычно отлаживать сценарии командной оболочки довольно легко, хотя специальных вспомогательных средств отладки не существует. В этом разделе мы дадим краткий обзор наиболее распространенных приемов.
Когда возникает ошибка, командная оболочка, как правило, выводит на экран номер строки, содержащей ошибку. Если ошибка сразу не видна, вы можете добавить несколько дополнительных команд echo
для отображения содержимого переменных и протестировать фрагменты программного кода, просто вводя их в командной оболочке в интерактивном режиме.
Поскольку сценарии обрабатываются интерпретатором, нет затрат на компиляцию при корректировке и повторном выполнении сценария. Основной способ отслеживания наиболее трудно выявляемых ошибок — задание различных опций командной оболочки. Для этого вы можете применять опции командной строки после запуска командной оболочки или использовать команду set
. В табл. 2.19 перечислены эти опции.
Таблица 2.19
Опция командной строки | Опция команды set | Описание |
---|---|---|
sh -n <сценарий> | set -о noexec set -n | Только проверяет синтаксические ошибки; не выполняет команды |
sh -v <сценарий> | set -о verbose set -v | Выводит на экран команды перед их выполнением |
sh -х <сценарий> | set -о xtrace set -x | Выводит на экран команды после обработки командной строки |
sh -u <сценарий> | set -o nounset set -u | Выдает сообщение об ошибке при использовании неопределенной переменной |
Вы можете установить опции с помощью флагов -о
и сбросить их с помощью флагов +о
подобным же образом в сокращенных версиях. Получить простое отслеживание выполнения можно, используя опцию xtrace
. Для начала вы можете применить опцию командной строки, но для более тщательной отладки следует поместить опции xtrace
(задавая выполнение и сброс отслеживания выполнения) внутрь сценария, в тот фрагмент кода, который создает проблему. Отслеживание выполнения заставляет командную оболочку перед выполнением каждой строки сценария выводить на экран эту строку и подставлять в нее значения используемых переменных.
Для установки опции xtrace
используйте следующую команду:
set -о xtrace
Для того чтобы снова отключить эту опцию, применяйте следующую команду:
set +о xtrace
Уровень выполняемых подстановок задается (по умолчанию) количеством знаков +
в начале каждой строки. Вы можете заменить знак +
на что-то более осмысленное, определив переменную командной оболочки PS4
в вашем файле конфигурации оболочки.
В командной оболочке также можно выяснить состояние программы после ее завершения, перехватив сигнал EXIT
с помощью строки, подобной приведенной далее и помещенной в начале вашего сценария:
trap 'echo Exiting: critical variable = $critical_variable' EXIT
Прежде чем мы закончим обсуждение сценариев командной оболочки, обратимся к еще одному средству, которое, хотя, строго говоря, и не является частью оболочки, в основном полезно применять именно в программах командной оболочки.
Если вы знаете, что ваш сценарий придется выполнять только с консоли ОС Linux, существует довольно изящный способ оживить сценарий, применяя служебную команду dialog
. Она использует средства псевдографики текстового режима и цвет, но при этом выглядит привлекательно.
В некоторых дистрибутивах команда dialog
по умолчанию не устанавливается; например, в Ubuntu вам, возможно, придется добавить совместно поддерживаемые репозитарии для поиска готовой версии. В других дистрибутивах вы можете найти уже установленный альтернативный вариант, gdialog
. Он очень похож, но рассчитан на пользовательский интерфейс GNOME, применяемый для отображения диалоговых окон команды. В этом случае вы получите настоящий графический интерфейс. Как правило, в любой программе, использующей команду dialog
, можно заменить все вызовы этой команды на gdialog
, и вы получите графическую версию вашей программы. В конце этого раздела мы покажем пример программы, использующей команду gdialog
.
Общая концепция утилиты dialog
проста — одна программа с множеством параметров и опций, позволяющих отображать различные типы графических окон, начиная с простых окон с кнопками типа Yes/No (Да/Нет) и заканчивая окнами ввода и даже выбором пункта меню. Утилита обычно возвращает результат, когда пользователь выполнил какой-либо ввод, и результат может быть получен или из статуса завершения, или, если вводился текст, извлечением стандартного потока ошибок.
Прежде чем переходить к подробностям, давайте рассмотрим очень простой пример применения утилиты dialog
. Вы можете вызывать ее непосредственно из командной строки, что прекрасно с точки зрения создания прототипов. Поэтому создадим простое окно сообщений для отображения традиционной первой программы:
dialog --msgbox "Hello World" 9 18
На экране появится графическое информационное окно, дополненное кнопкой OK (рис. 2.3).
Рис. 2.3
Теперь, когда вы убедились в простоте утилиты dialog
, давайте поподробнее рассмотрим ее функциональные возможности. Основные типы диалоговых окон, которые вы можете создавать, перечислены в табл. 2.20.
Таблица 2.20
Тип диалогового окна | Опция, применяемая для создания окна этого типа | Назначение окна |
---|---|---|
Окна с флажками (Check boxes) | --checklist | Позволяет отображать список флажков, каждый из которых можно установить или сбросить |
Информационные окна (Info boxes) | --infobox | Простое немедленное отображение в окне, без очистки экрана, возвращаемых данных |
Окна ввода (Input boxes) | --inputbox | Позволяет пользователю вводить в окно текст |
Окна меню (Menu boxes) | --menu | Позволяет пользователю выбрать один пункт из списка |
Окна сообщений (Message boxes) | --msgbox | Отображает сообщения для пользователей и снабжено кнопкой OK, которую они должны нажать для продолжения |
Окна с переключателями (Radio selection boxes) | --radiolist | Позволяет пользователю выбрать один переключатель из списка |
Текстовые окна (Text boxes) | --textbox | Позволяют отображать содержимое файла в окне с прокруткой |
Диалоговые окна Да/Нет (Yes/No boxes) | --yesno | Позволяют задать вопрос, на который пользователь может ответить "Да" или "Нет" |
Доступны также и некоторые дополнительные типы диалоговых окон (например, индикатор процесса или окно ввода пароля). Если вы хотите узнать побольше о необычных типах диалоговых окон, как всегда подробную информацию можно найти на страницах интерактивного справочного руководства.
Для получения вывода из диалогового окна любого типа, допускающего текстовый ввод или выбор, вы должны перехватить стандартный поток ошибок, как правило, направляя его во временный файл, который вы сможете обработать позже. Для получения ответа на вопросы типа "Да"/"Нет", просто проверьте код завершения, который, как и во всех соблюдающих приличия программах, в случае успеха возвращает 0 (т. е. выбор ответа "Да" (Yes)) и 1 в остальных случаях.
У всех типов диалоговых окон есть дополнительные управляющие параметры, такие как размер и контур отображаемого окна. В табл. 2.21 перечислены разные параметры, необходимые окнам каждого типа, а затем демонстрируется использование некоторых из них в командной строке. В заключение вы увидите простой пример, объединяющий в одной программе несколько диалоговых окон разных типов.
Таблица 2.21
Тип диалогового окна | Параметры |
---|---|
--checklist | text height width list-height [tag text status] ... |
--infobox | text height width |
--inputbox | text height width [initial string] |
--menu | text height width menu-height [tag item ] ... |
--msgbox | text height width |
--radiolist | text height width list-height [tag text status] ... |
--textbox | filename height width |
--yesno | text height width |
Помимо параметров диалоговые окна всех типов принимают ряд опций. Мы не будем перечислять все, а упомянем лишь две из них: --title
, позволяющую задать заголовок окна, и -clear
, применяемую по прямому назначению, т.е. для очистки экрана. Полный список опций см. на страницах интерактивного справочного руководства.
Выполните упражнения 2.21 и 2.22.
Давайте сразу перейдем к красивому сложному примеру. Если вы поймете его, все остальные покажутся легкими! В этом примере вы создадите диалоговое окно со списком флажков, с заголовком Check me (Поставь галочку) и пояснительной надписью Pick Numbers (Выбери номера). Окно с флажками будет высотой 15 строк и шириной 25 символов, и каждый флажок будет занимать 3 символа по высоте. И последнее, но не по степени важности, вы перечислите отображаемые элементы вместе с принятой по умолчанию установкой или сбросом (on/off) флажка.
dialog --title "Check me" --checklist "Pick Numbers" 15 25 3 1 "one" "off" 2 "two" "on" 3 "three" "off"
Полученный результат показан на рис. 2.4.
Как это работает
В этом примере параметр --checklist
указывает на то, что вы собираетесь создать диалоговое окно с флажками. Вы используете опцию --title
для задания заголовка "Check me
", следующий параметр — пояснительная надпись "Pick Numbers
".
Далее вы переходите к указанию размеров диалогового окна. Оно будет высотой 15 строк и шириной 25 символов и 3 строки отводятся для меню. Это не самый удачный выбор размеров, но он позволит вам увидеть, как размещаются элементы.
Опции выглядят несколько мудрено, но вам следует запомнить только то, что у каждого элемента списка есть три значения:
□ номер в списке;
□ текст;
□ состояние.
Рис. 2.4
У первого элемента номер 1, отображается текст "one" (один) и выбрано состояние "off" (сброшен). Далее вы переходите ко второму элементу с номером 2, текстом "two" и состоянием "on" (установлен). Так продолжается до тех пор, пока вы не опишите все элементы списка.
Легко, не правда ли? Теперь попробуйте ввести несколько вариантов в командной строке и убедитесь, насколько эту утилиту легко применять. Для того чтобы включить этот пример в программу, вы должны иметь доступ к результатам пользовательского ввода. Это совсем просто: перенаправьте стандартный поток ошибок в текстовый ввод или проверьте переменную окружения $?
, которая, как вы помните, не что иное, как код завершения предыдущей команды.
Давайте рассмотрим простую программу questions, которая принимает к сведению пользовательские ответы.
1. Начните с простого диалогового окна, сообщающего пользователю о происходящем. Вам не нужно получать результат или какой бы то ни было ввод пользователя, поэтому все просто и ясно:
#!/bin/sh
# Задайте несколько вопросов и получите ответ
dialog --title "Questionnaire" --msgbox "Welcome to my simple survey" 9 18
2. Спросите пользователя с помощью простого диалогового окна с кнопками типа Yes/No, хочет ли он продолжать. Воспользуйтесь переменной окружения $?
для того, чтобы выяснить, выбрал пользователь ответ Yes (код завершения 0) или No. Если он не хочет двигаться дальше, используйте простое информационное окно, не требующее никакого пользовательского ввода для своего завершения.
dialog --title "Confirm" --yesno "Are you willing to take part?" 9 18
if [ $? != 0 ]; then
dialog --infobox "Thank you anyway" 5 20 sleep 2
dialog --clear exit 0
fi
3. Спросите у пользователя его имя с помощью диалогового окна ввода. Перенаправьте стандартный поток ошибок во временный файл _1.txt, который затем вы сможете обработать в переменной QNAME
.
dialog --title "Questionnaire" --inputbox "Please enter your name" 9 30 2>_1.txt
Q_NAME=$(cat _1.txt)
4. Здесь у вас появляется меню из четырех пунктов. И снова вы перенаправляете стандартный поток ошибок и загружаете его в переменную.
dialog --menu "$Q_NAME, what music do you like best?" 15 30 4 1 "Classical" 2 "Jazz" 3 "Country" 4 "Other" 2>_1.txt
Q_MUSIC=$(cat _1.txt)
5. Номер, выбранный пользователем, будет запоминаться во временном файле _1.txt, который перехватывается переменной Q_MUSIC
, поэтому вы сможете проверить результат.
if [ "$Q_MUSIC" = "1" ]; then
dialog --title "Likes Classical" --msgbox "Good choice!" 12 25
else
dialog --title "Doesn't like Classical" --msgbox "Shame" 12 25
fi
В заключение очистите последнее диалоговое окно и завершите программу.
sleep 2
dialog --clear
exit 0
На рис. 2.5 показан результат.
Как это работает
В данном примере вы соединяете команду dialog
и простой программный код на языке командной оболочки для того, чтобы показать, как можно создавать простые программы с графическим пользовательским интерфейсом, используя только сценарий командной оболочки. Вы начинаете с обычного экрана-приветствия, а затем с помощью простого диалогового окна с кнопками типа Yes/No спрашиваете пользователя о его желании участвовать в опросе. Вы используете переменную $?
для проверки ответа пользователя. Если он согласен, вы запрашиваете его имя, сохраняете его в переменной Q_NAME
и выясняете с помощью диалогового окна-меню, какой музыкальный стиль он любит. Сохранив числовой вывод в переменной Q_MUSIC
, вы сможете увидеть, что ответил пользователь, и отреагировать соответственно.
Рис. 2.5
Рис. 2.6
Если вы применяете графический пользовательский интерфейс (GUI) на базе графической среды GNOME и в данный момент запустили в нем сеанс работы с терминалом, на месте команды dialog
можно использовать команду gdialog
. У обеих команд одинаковые параметры, поэтому вы сможете воспользоваться тем же программным кодом, не считая замены запускаемой вами команды dialog
командой gdialog
. На рис. 2.6 показано, как выглядит этот сценарий в дистрибутиве Ubuntu, когда применяется команда gdialog
.
Это очень лёгкий способ формирования из сценария удачного графического пользовательского интерфейса.
Теперь, когда вы познакомились с основными функциональными возможностями командной оболочки как языка программирования, пора написать программу, в которой используется кое-что из того, чему вы научились.
На протяжении всей книги вы будете пытаться разработать приложение управления базой данных компакт-дисков для того, чтобы продемонстрировать приемы и методы, которые вы только что освоили. Вы начинаете со сценария командной оболочки, но очень скоро вы будете делать то же самое на языке программирования С, добавлять базу данных и т.д.
Предположим, что у вас есть разнообразная коллекция компакт-дисков. Для того чтобы облегчить себе жизнь, вы собираетесь разработать и реализовать программу управления компакт-дисками. Электронный каталог представляется идеальным проектом для реализации, когда вы учитесь программированию в ОС Linux.
Вначале вы хотите, по меньшей мере, хранить некоторые основные сведения о каждом компакт-диске, такие как название, музыкальный стиль и исполнитель или композитор. Возможно, вам также хотелось бы запоминать некоторую простую информацию о дорожках. Вы хотите иметь возможность находить любое музыкальное произведение из хранящихся на компакт-диске, но не какие-то подробности, касающиеся дорожек. Для придания законченности мини-приложению вам хотелось бы иметь возможность в самом приложении вводить, обновлять и удалять любую часть информации.
Поиск и отображение данных — заставляют предполагать, что адекватным решением будет простое меню. Все данные, которые следует хранить, — текстовые, и, полагая, что ваша коллекция компакт-дисков не слишком велика, для управления ею не потребуется сложная база данных, подойдут простые текстовые файлы. Сохранение информации в текстовых файлах позволит сделать приложение простым, и если ваши требования изменятся, всегда легче манипулировать текстовыми файлами, чем какими бы то ни было еще. И, в крайнем случае, вы сможете применить редактор для ввода и удаления данных вручную, а не писать для этого специальную программу.
Необходимо принять важное проектное решение, касающееся способа хранения данных. Достаточно одного файла? Если да, то какой у него должен быть формат? Большая часть информации, которую вы собираетесь хранить, за исключением данных о дорожках, вводится однократно для каждого компакт-диска (мы пока оставим в стороне вариант наличия на одном CD произведений разных композиторов и исполнителей). И практически на всех компакт-дисках много дорожек.
Нужно ли ограничить количество дорожек на одном компакт-диске, которые можно хранить? Это ограничение кажется лишним, поэтому сразу отбросим его.
Если вы допускаете, что на компакт-диске может быть разное количество дорожек, у вас есть три варианта:
□ использовать один файл с одной строкой для "заголовочной" типовой информации и n строк для сведений о дорожках на каждом компакт-диске;
□ поместить всю информацию о каждом компакт-диске в одну строку, разрешая ей продолжаться то тех пор, пока вся информация о дорожках диска не будет сохранена;
□ отделить заголовочную информацию от данных о дорожках и для каждого типа информации использовать отдельный файл.
Только третий вариант позволит нам легко изменять формат файлов, что потребуется, если вы когда-либо захотите превратить вашу базу данных в реляционную (более подробную информацию об этом см. в главе 7), поэтому выберем этот вариант.
Далее нужно решить, какие данные помещать в файлы.
Сначала вы выбираете для заголовка каждого компакт-диска хранение следующей информации:
□ номер компакт-диска в каталоге;
□ название;
□ музыкальный стиль (классика, рок, поп, джаз и т.д.);
□ композитор или исполнитель.
О дорожках вы будете хранить две характеристики:
□ номер дорожки;
□ ее название.
Для объединения двух файлов вы должны сопоставить данные о дорожках с остальной информацией о компакт-диске. Для этого будет использоваться номер компакт-диска в каталоге. Поскольку он уникален для каждого диска, он будет появляться однократно в файле с заголовками и один раз для каждой дорожки в файле с данными о дорожках.
В табл. 2.22 показан пример файла с заголовочными данными, а соответствующий ему файл с данными о дорожках может выглядеть так, как представлено в табл. 2.23.
Таблица 2.22
Catalog | Title | Type | Composer |
---|---|---|---|
CD123 | Cool sax | Jazz | Bix |
CD234 | Classic violin | Classical | Bach |
CD345 | Hits99 | Pop | Various |
Таблица 2.23
Catalog | Track No. | Title |
---|---|---|
CD123 | 1 | Some jazz |
CD123 | 2 | More jazz |
CD234 | 1 | Sonata in D minor |
CD345 | 1 | Dizzy |
Два файла объединены общим полем Catalog
(Каталог). Следует помнить о том, что обычно на одну строку файла с заголовочной информацией приходится много строк в файле с данными о дорожках.
Последнее, что мы должны решить, — способ разделения элементов данных. Поля фиксированной ширины, обычные в реляционных базах, — не всегда самый удобный вариант. Другой распространенный способ, применяемый в данном примере, — запятая (т. е. файл со значениями, разделенными запятыми, или CSV-файл).
В упражнении 2.23 только для того, чтобы вы окончательно не запутались, применяются следующие функции:
□ get_return()
;
□ get_confirm()
;
□ set_menu_choice()
;
□ insert_title()
;
□ insert_track()
;
□ add_record_tracks()
;
□ add_records()
;
□ find_cd()
;
□ update_cd()
;
□ count_cds()
;
□ remove_records()
;
□ list_tracks()
.
1. Сначала в примере сценария как всегда стоит строка, обеспечивающая его выполнение как сценария командной оболочки, за которой следует некоторая информация об авторских правах:
#!/bin/bash
# Очень простой пример сценария командной оболочки для управления
# коллекцией компакт-дисков.
# Copyright (С) 1996-2007 Wiley Publishing Inc.
# Это свободно распространяемое программное обеспечение;
# вы можете распространять эту программу и/или изменять ее
# в соответствии с положениями GNU General Public License,
# документа, опубликованного фондом Free Software Foundation;
# либо версии 2 этой лицензии или (по вашему выбору)
# любой более свежей версии.
# Эта программа распространяется в надежде на ее полезность,
# но WITHOUT ANY WARRANTY, (без каких-либо гарантий);
# даже без предполагаемой гарантии MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE (годности
# ее для продажи или применения для определенной цели).
# Более подробную информацию см. в GNU General Public License.
# Вы должны были получить копию GNU General Public License
# вместе с этой программой;
# если нет, пишите в организацию Free Software Foundation,
# Inc. no адресу: 675 Mass Ave, Cambridge, MA 02139, USA.
2. Теперь убедитесь, что установлены некоторые глобальные переменные, которые будут использоваться во всем сценарии. Задайте заголовочный файл, файл с данными о дорожках и временный файл и перехватите нажатие комбинации клавиш <Ctrl>+<C> для того, чтобы удалить временный файл, если пользователь прервет выполнение сценария.
menu_choice=""
current cd=""
title_file="title.cdb"
tracks_file="tracks.cdb"
temp_file=/tmp/cdb.$$
trap 'rm -f $temp_file' EXIT
3. Определите ваши функции так, чтобы сценарий, выполняясь с первой строки, мог найти все определения функций до того, как вы попытаетесь вызвать любую из них в первый раз. Для того чтобы не повторять один и тот же программный код в нескольких местах, сначала вставьте две функции, служащие простыми утилитами:
get_return() (
echo -е "Press return \с"
read x
return 0
}
get_confirm() (
echo -e "Are you sure? \c"
while true do
read x
case "$x" in
y | yes | Y | Yes | YES )
return 0;;
n | no | N | No | NO )
echo
echo "Cancelled"
return 1;;
*)
echo "Please enter yes or no" ;;
esac
done
}
4. Теперь вы дошли до основной, формирующей меню функции set_menu_choice
. Содержимое меню изменяется динамически, добавляя дополнительные пункты при выборе компакт-диска.
set_menu_choice() {
clear
echo "Options :-"
echo
echo " a) Add new CD"
echo " f) Find CD"
echo " c) Count the CDs and tracks in the catalog"
if [ "$cdcatnum" != "" ]; then
echo " 1) List tracks on $cdtitle"
echo " r) Remove $cdtitle"
echo " u) Update track information for $cdtitle"
fi
echo " q) Quit" echo
echo -e "Please enter choice then press return \c"
read menu_choice
return
}
Имейте в виду, что команда echo -е
не переносится в некоторые командные оболочки.
5. Далее идут две очень короткие функции, insert_title
и insert_track
, для пополнения файлов базы данных. Несмотря на то, что некоторые программисты ненавидят однострочные функции вроде этих, они помогают сделать понятнее другие функции.
За ними следует более длинная функция add_record_track
, использующая эти функции. В данной функции применяется проверка на соответствие шаблону, чтобы исключить ввод запятых (поскольку мы. используем запятые для разделения полей), и арифметические операции для увеличения номера текущей дорожки на 1, когда вводятся данные о новой дорожке.
insert_title() {
echo $* >> $title_file
return
}
insert_track() {
echo $* >> $tracks_file
return
}
add_record_tracks() {
echo "Enter track information for this CD"
echo "When no more tracks enter q"
cdtrack=1
cdttitle=""
while [ "$cdttitle" != "q" ]
do
echo -e "Track $cdtrack, track title? \c"
read tmp
cdttitle=${tmp%%, *}
if [ "$tmp" != "$cdttitle" ]; then
echo "Sorry, no commas allowed"
continue
fi
if [ -n "$cdttitle" ] ; then
if [ "$cdttitle" ! = "q" ]; then
insert_track $cdcatnum, $cdtrack, $cdttitle
fi
else
cdtrack=$((cdtrack-1))
fi
cdtrack=$((cdtrack+1))
done
}
6. Функция add_records
позволяет вводить основную информацию о новом компакт-диске.
add_records() {
# Подсказка для начала ввода информации
echo -е "Enter catalog name \с"
read tmp
cdcatnum=${tmp%%, *}
echo -e "Enter title \c"
read tmp
cdtitle=${tmp%%, *}
echo -e "Enter type \c"
read tmp
cdtype=${tmp%%, *}
echo -e "Enter artist/composer \c"
read tmp
cdac=${tmp%%, *}
# Проверяет, хочет ли пользователь ввести информацию
echo About to add new entry
echo "$cdcatnum $cdtitle $cdtype $cdac"
# Если получено подтверждение, добавляет данные в конец файла.
# с заголовками
if get_confirm ; then
insert_title $cdcatnum, $cdtitle, $cdtype, $cdac
add_record_tracks
else
remove_records
fi
return
}
7. Функция find_cd
с помощью команды grep
ищет текст с названием компакт-диска в файле с заголовочной информацией. Вам нужно знать, сколько раз была найдена строка, а команда grep только вернет значение, указывающее на то, что строка не была найдена или была найдена многократно. Для решения этой проблемы сохраните вывод в файл, отводящий по одной строке на каждое найденное совпадение, а затем сосчитайте количество строк в файле.
У команды счетчика слов, wc
, в выводе есть пробельный символ, разделяющий количества строк, слов и символов в файле. Используйте синтаксическую запись $(wc -l $temp_file)
для извлечения первого параметра в выводе и переноса его в переменную linesfound
. Если бы вам был нужен другой следующий далее параметр, нужно было бы воспользоваться командой set
для установки значений переменных-параметров оболочки из вывода команды.
Изменив значение переменной IFS
(Internal Field Separator, внутренний разделитель полей) на запятую, вы сможете разделить поля, разграниченные запятыми. Альтернативный вариант — применить команду cut
.
find_сd() {
if [ "$1" = "n" ]; then
asklist=n
else
asklist=y
fi
cdcatnum=""
echo -e "Enter a string to search for in the CD titles \c"
read searchstr
if [ "$searchstr" = "" ]; then
return 0
fi
grep "$searchstr" $title_file > $temp_file
set $(wc -l $temp_file)
linesfound=$1
case "$linesfound" in
0)
echo "Sorry, nothing found"
get_return
return 0 ;;
1) ;;
2)
echo "Sorry, not unique."
echo "Found the following"
cat $temp_file
get_return
return 0
esac
IFS=", "
read cdcatnum cdtitle cdtype cdac < $temp_file
IFS=" "
if [ -z "$cdcatnum" ]; then
echo "Sorry, could not extract catalog field from $temp_file"
get_return
return 0
fi
echo
echo Catalog number: $cdcatnum echo Title: $cdtitle
echo Type: $cdtype
echo Artist/Composer: $cdac
echo
get_return
if [ "$asklist" = "y" ]; then
echo -e "View tracks for this CD? \c"
read x
if [ "$x" = "y" ]; then
echo
list_tracks
echo
fi
fi
return 1
}
8. Функция update_cd
позволит вам повторно ввести сведения о компакт-диске. Учтите, что вы ищите (с помощью команды grep
) строки, начинающиеся (^
) с подстроки $cdcatnum
, за которой следует ", "
и должны заключить подстановку значения $cdcatnum
в {}
. Таким образом, вы сможете найти запятую без специального пробельного символа между ней и номером в каталоге. Эта функция также использует {}
для образования блока из нескольких операторов, которые должны выполняться, если функция get_confirm
вернет значение true.
update_cd() {
if [ -z "$cdcatnum" ]; then
echo "You must select a CD first"
find_cd n
fi
if [ -n "$cdcatnum" ]; then
echo "Current tracks are :-"
list_tracks
echo
echo "This will re-enter the tracks for $cdtitle"
get_confirm && {
grep -v "^${cdcatnum}, " $tracks_file > $temp_file
mv $temp_file $tracks_file
echo
add_record_tracks
}
fi
return
}
9. Функция count_cds
дает возможность быстро пересчитать содержимое базы данных.
count_cds() {
set $(wc -l $title_file)
num_titles=$1
set $(wc -l $tracks_file)
num_tracks=$1
echo found $num_titles CDs, with a total of $num_tracks tracks
get_return
return
}
10. Функция remove_records
удаляет элементы из файлов базы данных с помощью команды grep -v
, удаляющей все совпадающие строки. Учтите, что нужно применять временный файл.
Если вы попытаетесь применить команду:
grep -v "^$cdcatnum" > $title_file
файл $title_file
станет пустым благодаря перенаправлению вывода >
до того, как команда grep
выполнится, поэтому она будет читать уже пустой файл.
remove_records() {
if [ -z "$cdcatnum" ]; then
echo You must select a CD first find_cd n
fi
if [ -n "$cdcatnum" ]; then
echo "You are about to delete $cdtitle"
get_confirm && {
grep -v "^${cdcatnum}, " $title_file > $temp_file
mv $temp_file $title_file
grep -v "^${cdcatnum}, " $tracks_file > $temp_file
mv $temp_file $tracks_file
cdcatnum=""
echo Entry removed
}
get_return
fi
return
}
11. Функция list_tracks
снова использует команду grep
для извлечения нужных вам строк, команду cut
для доступа к отдельным полям и затем команду more
для постраничного вывода. Если вы посмотрите, сколько строк на языке С займет повторная реализация этих 20 необычных строк кода, то поймете, каким мощным средством может быть командная оболочка.
list_tracks() {
if [ "$cdcatnum" = "" ]; then
echo no CD selected yet
return
else
grep "^${cdcatnum}, " $tracks_file > $temp_file
num_tracks=${wc -l $temp_file}
if [ "$num_tracks" = "0" ]; then
echo no tracks found for $cdtitle
else
{
echo
echo "$cdtitle :-"
echo
cut -f 2- -d , $temp_file
echo
} | ${PAGER:-more}
fi
fi
get_return
return
}
12. Теперь, когда все функции определены, можно вводить основную процедуру. Первые несколько строк просто приводят файлы в известное состояние; затем вы вызываете функцию формирования меню set_menu_choice
и действуете в соответствии с ее выводом.
Если выбран вариант quit
(завершение), вы удаляете временный файл, выводите сообщение и завершаете сценарий с успешным кодом завершения.
rm -f $temp_file
if [ ! -f $title_file ]; then
touch $title_file
fi
if [ ! -f $tracks_file ]; then
touch $tracks_file
fi
# Теперь непосредственно приложение
clear
echo
echo
echo "Mini CD manager" sleep 1
quit=n
while [ "$quit" != "y" ]; do
set_menu_choice
case "$menu_choice" in
a) add_records;;
r) remove records;;
f) find_cd y;;
u) update_cd;;
c) count_cds;;
l) list_tracks;;
b)
echo
more $title_file
echo
get return;;
q | Q ) quit=y;;
*) echo "Sorry, choice not recognized";;
esac
done
# Убираем и покидаем
rm -f $temp_file echo "Finished"
exit 0
Команда trap в начале сценария предназначена для перехвата нажатия пользователем комбинации клавиш <Ctrt>+<C>. Им может быть сигнал EXIT
или INT
, в зависимости от настроек терминала.
Существуют другие способы реализации выбора пункта меню, особенно конструкция select
в оболочках bash и ksh (которая, тем не менее, не определена в стандарте X/Open). Она представляет собой специализированный селектор пунктов меню. Проверьте ее на практике, если ваш сценарий может позволить себе быть немного менее переносимым. Для передачи пользователям многострочной информации можно также воспользоваться встроенными документами.
Возможно, вы заметили, что нет проверки первичного ключа, когда создается новая запись; новый код просто игнорирует последующие названия с тем же кодом, но включает их дорожки в перечень первого названия:
1 First CD Track 1
2 First CD Track 2
1 Another CD
2 With the same CD key
Мы оставляем это и другие усовершенствования в расчете на ваше воображение и творческие способности, которые проявятся при корректировке вами программного кода в соответствии с требованиями GPL.
В этой главе вы увидели, что командная оболочка — это мощный язык программирования со своими функциональными возможностями. Ее способность легко вызывать программы и затем обрабатывать их результат делают оболочку идеальным средством для решения задач, включающих обработку текста и файлов.
Теперь, если вам понадобится небольшая утилита, подумайте, сможете ли вы решить вашу проблему, комбинируя множество команд ОС Linux в сценарии командной оболочки. Вы будете поражены, увидев, как много вспомогательных программ можно написать без использования компилятора.