Дейв Тейлор, Брендон Перри Сценарии командной оболочки. Linux, OS X и Unix 2-е издание
Dave Taylor, Brandon Perry
Wicked Cool Shell Scripts
101 Scripts for Linux, Mac OS X, and UNIX Systems
2nd Edition
Перевел с английского А. Киселев
Заведующая редакцией Ю. Сергиенко
Ведущий редактор Н. Римицан
Литературный редактор А. Петров
Художественный редактор С. Заматевская
Корректор И. Тимофеева
Верстка Л. Егорова
© 2017 by Dave Taylor and Brandon Perry
© Перевод на русский язык ООО Издательство «Питер», 2017
© Издание на русском языке, оформление ООО Издательство «Питер», 2017
© Серия «Для профессионалов», 2017
Об авторах
Дейв Тейлор (Dave Taylor) работает в компьютерной индустрии с 1980 года. Участвовал в создании BSD 4.4 UNIX, его программы включены во все основные дистрибутивы UNIX. Выдающийся оратор и автор тысяч статей для журналов и газет. Написал более 20 книг, включая «Learning Unix for OS X» (O’Reilly Media), «Solaris 9 for Dummies» (Wiley Publishing) и «Sams Teach Yourself Unix in 24 Hours» (Sams Publishing). Популярный колумнист журнала «Linux Journal» и основатель веб-сайта askdavetaylor.com, где осуществляет техническую поддержку пользователей и выкладывает обзоры новых гаджетов.
Брендон Перри (Brandon Perry) начал писать приложения на C# с выходом открытой реализации. NET — Mono. В свободное время любит писать модули для фреймворка Metasploit, исследовать двоичные файлы и тестировать всякие штуки.
О научном рецензенте
Джорди Гутьеррес Эрмосо (Jordi Gutiérrez Hermoso) — программист, математик и вольный хакер. Начиная с 2002 года пользуется исключительно Debian GNU/Linux не только дома, но и на работе. Джорди участвует в разработке GNU Octave, бесплатной вычислительной среды, во многом совместимой с Matlab, а также Mercurial, распределенной системы управления версиями. Увлекается чистой и прикладной математикой, катанием на коньках, плаванием и вязанием. В последнее время много думает о проблемах выброса парниковых газов и участвует в акциях по сохранению носорогов.
Благодарности для первого издания
В создании книги участвовало удивительно большое количество людей, но особенно хотелось бы отметить Ди-Анн Лебланк (Dee-Ann LeBlanc), научного рецензента первых вариантов рукописи и неутомимого собеседника в IM, и Ричарда Блюма (Richard Blum), научного редактора и опытнейшего разработчика сценариев, представившего важные комментарии к подавляющему большинству сценариев в книге. Нат Торкингтон (Nat Torkington) помог с организацией и надежностью сценариев. В числе других, оказавших неоценимую помощь в процессе работы над книгой, можно назвать Одри Бронфин (Audrey Bronfin), Мартина Брауна (Martin Brown), Брайана Дея (Brian Day), Дейва Энниса (Dave Ennis), Вернера Клаузера (Werner Klauser), Юджина Ли (Eugene Lee), Энди Лестера (Andy Lester) и Джона Мейстера (John Meister). Немало полезного было почерпнуто на форумах MacOSX.com (отличное место для общения), а коллектив AnswerSquad.com предложил много ценных советов и предоставил бесконечные возможности для отдыха. Наконец, эта книга не оказалась бы в ваших руках без поддержки Билла Поллока (Bill Pollock) и помощи Хиллеля Хинштейна (Hillel Heinstein), Ребекки Пеппер (Rebecca Pepper) и Кэрол Жардо (Karol Jurado): спасибо всему коллективу No Starch Press!
Я хотел бы поблагодарить за поддержку моих замечательных детей — Эшли (Ashley), Гарета (Gareth) и Киану (Kiana) — а также обитателей нашего домашнего зверинца.
Благодарности для второго издания
За последние десять лет «Сценарии командной оболочки» зарекомендовала себя как нужная и полезная книга для тех, кто увлекается созданием сценариев на языке командной оболочки bash или желает освоить более эффективные приемы. В обновленном втором издании Дейв и я надеялись дать этой книге второе дыхание и вдохновить других на еще одно десятилетие экспериментов со сценариями командной оболочки. Эту работу, в ходе которой были добавлены новые сценарии и уточнены многие формулировки, нам не удалось бы проделать без поддержки многих и многих.
Хочу сказать спасибо своему коту Сэму, который сидел на ноутбуке, пока я пытался работать. Полагаю, он был уверен, что помогает мне. Мои друзья и семья с пониманием отнеслись к тому, что я добрых нескольких месяцев говорил только о сценариях bash. Коллектив издательства No Starch Press был необычайно благосклонен ко мне, не писавшему ничего крупнее заметки в школьную газету или статьи в блог, поэтому огромное спасибо Биллу Поллоку (Bill Pollock), Лиз Чадвик (Liz Chadwick), Лорел Чан (Laurel Chun) и всем остальным сотрудникам No Starch. Комментарии Джорди Гутьерреса Эрмосо, касающиеся технических аспектов книги и программного кода, были для меня более чем ценными.
С момента первой публикации этой книги в 2004 году в мире администрирования системы Unix произошли огромные изменения. В то время лишь немногие пользователи устанавливали на свои компьютеры Unix-подобные операционные системы. Но с появлением дружественных к начинающим дистрибутивов Linux, таких как Ubuntu, ситуация стала меняться. Затем появилась OS X, следующее поколение операционной системы компании Apple, основанной на Unix, за ней последовало множество технологий на основе iOS. В настоящее время Unix-подобные операционные системы получили более широкое признание. Фактически они стали самыми вездесущими в мире, если принять во внимание Android — операционную систему для смартфонов.
Излишне говорить, что многое изменилось, но одно остается неизменным — командная оболочка Bourne-again shell, или bash, сохраняет свои позиции основной командной оболочки в Unix. Использование всех возможностей ее языка никогда прежде не было такой насущной необходимостью для системных администраторов, инженеров и энтузиастов.
Что исчезло во втором издании
В этой книге описываются типичные сложности, с которыми можно столкнуться при попытке написать переносимое автоматизированное решение, например, для сборки программного обеспечения или координации действий других программ, и способы их преодоления. Решения в книге подаются так, чтобы вы могли взять их за основу и экстраполировать на другие схожие задачи. Например, в главе 1 мы напишем переносимую версию программы echo в виде небольшого сценария-обертки. Многим системным администраторам может пригодиться этот конкретный сценарий, но основная идея заключается в том, чтобы создать сценарий-обертку, гарантирующий единообразие поведения на разных платформах. Далее в книге мы разберем некоторые интересные особенности сценариев на языке bash и типичные утилиты, доступные в системах Unix и дающие нам самые широкие возможности.
Эта книга для вас, если…
Bash остается основным инструментом для всех, кто работает с серверами или рабочими станциями, действующими под управлением Unix-подобных операционных систем, в том числе и для веб-разработчиков (многие из которых ведут разработку в OS X и развертывают свои приложения на серверах под Linux), аналитиков, разработчиков мобильных приложений и программистов. Кроме того, все больше появляется энтузиастов, запускающих Linux на своих микрокомпьютерах с открытой архитектурой, таких как Raspberry Pi, для автоматизации бытовых приборов. Сценарии командной оболочки отлично походят для всех этих случаев.
Представленные в книге сценарии будут, безусловно, полезны и тем, кто желает расширить и без того немалый опыт владения bash за счет изучения практических примеров, и тем, кто пользуется терминалом или сценариями командной оболочки лишь изредка. Если вы принадлежите ко второму лагерю, вам, вероятно, потребуется освежить знания или дополнить их, прочитав введение в продвинутые возможности bash.
Эта книга — не учебник! Наша цель — продемонстрировать практические приемы программирования сценариев на bash и познакомить с распространенными утилитами на (в большинстве) коротких и компактных примерах, но мы не описываем их строку за строкой. Мы объясняем только самые основные части, а опытные создатели сценариев смогут сами понять, как действует остальной код, прочитав его. Мы надеемся, что вы, уважаемый читатель, будете экспериментировать с этими сценариями — ломать их, исправлять и приспосабливать под свои нужды — чтобы понять, как они работают. Главная наша цель — показать, как решать типичные задачи, такие как управление сетью или синхронизация файлов, которые встают перед любым техническим специалистом.
Структура книги
Это второе издание включает дополненные оригинальные 12 глав и 3 новые главы. Каждая глава демонстрирует новые особенности или варианты использования сценариев командной оболочки, и вместе они охватывают всю широту возможностей сценариев для более простой работы в Unix. Большинство сценариев, представленных в книге, будет работать и в Linux, и в OS X. В иных случаях мы напишем об этом прямо.
Глава 0: Краткое введение в сценарии командной оболочки
Это совершенно новая глава, появившаяся во втором издании, которая послужит начинающим пользователям Unix кратким введением в синтаксис языка командной оболочки bash и особенности его использования. Эта глава быстро и без лирических отступлений расскажет все, что потребуется для успешного чтения главы 1: от простого определения сценариев командной оболочки до создания и выполнения незамысловатых примеров.
Глава 1: Отсутствующая библиотека
Языки программирования, широко используемые в окружении Unix, такие как C, Perl и Python, имеют обширные библиотеки разнообразных функций и утилит для проверки форматов чисел, вычисления интервалов времени между датами и решения многих других задач. Но, работая с командной оболочкой, мы почти со всем вынуждены справляться самостоятельно, поэтому в данной главе рассказывается об инструментах и приемах, которые сделают сценарии командной оболочки более дружественными. Все, что вы узнаете в первой главе, поможет вам читать сценарии, с которыми вы встретитесь в этой книге, и писать свои. Мы включили сюда разные функции проверки ввода, простой и мощный интерфейс к bc, инструмент быстрого добавления запятых для улучшения читаемости больших чисел, прием для разновидностей Unix, в которых команда echo не поддерживает полезный флаг −n, и сценарий для использования ANSI-последовательностей определения цвета в сценариях.
Главы 2 и 3: Усовершенствование пользовательских команд и Создание утилит
Эти две главы представляют новые команды, дополняющие и расширяющие стандартный инструментарий Unix. В конце концов, постоянное развитие и совершенствование — одна из отличительных черт Unix. Мы также причастны к этому процессу и в главах 2 и 3 предлагаем сценарии, которые реализуют: дружественный интерактивный калькулятор, инструмент удаления файлов, не стирающий их с диска, две системы напоминаний и слежения за событиями, усовершенствованную версию команды locate, команду date с поддержкой нескольких часовых поясов и новую версию команды ls, добавляющую в списки содержимого каталогов дополнительные данные.
Глава 4: Тонкая настройка Unix
Может прозвучать как ересь, но некоторые аспекты Unix выглядят недоработанными даже спустя десятилетия развития. Если вам доведется пользоваться разными версиями Unix, например переходить со свободно распространяемых дистрибутивов Linux на коммерческие версии Unix, такие как OS X, Solaris или Red Hat, вы столкнетесь с отсутствующими флагами и командами, с противоречивым поведением некоторых команд и другими подобными проблемами. Поэтому в данной главе будут представлены переделанные версии и интерфейсы к командам Unix, которые делают их чуть более дружественными или более согласованными с другими разновидностями Unix. Среди всего прочего здесь описывается способ добавления длинных флагов в стиле GNU в команды, не являющиеся командами GNU. Здесь же вы найдете пару интеллектуальных сценариев, упрощающих работу с разными утилитами сжатия файлов.
Главы 5 и 6: Системное администрирование: управление пользователями и обслуживание системы
Если вас заинтересовала наша книга, вполне вероятно, что у вас есть привилегии администратора и вы несете ответственность за администрирование одной или нескольких систем Unix, даже если речь идет всего лишь о персональном компьютере с Ubuntu или BSD. Эти две главы содержат несколько сценариев, которые помогут вам в администрировании, в том числе: утилиты для анализа использования дискового пространства, система дисковых квот, которая автоматически извещает пользователей по электронной почте о превышении выделенного им места на диске, улучшенная реализация команды killall, сценарий проверки crontab, инструмент ротации файлов журналов и пара утилит для создания резервных копий.
Глава 7: Пользователи Интернета
Эта глава включает пакет по-настоящему интересных сценариев командной оболочки, демонстрирующих некоторые замечательные и простые приемы использования командной строки Unix для работы с ресурсами в Интернете. В том числе: инструмент для извлечения адресов URL из любой веб-страницы, инструмент для получения прогноза погоды, инструмент поиска в базах данных видеофильмов и инструмент для обнаружения изменений на веб-сайте, который автоматически сообщает о них по электронной почте.
Глава 8: Инструменты веб-мастера
Если вы веб-мастер и поддерживаете веб-сайт, действующий в вашей собственной системе Unix или на удаленном сервере где-то в сети, в этой главе вы найдете очень интересные инструменты для конструирования веб-страниц на лету, создания веб-альбомов с фотографиями и даже журналирования результатов веб-поиска.
Главы 9 и 10: Администрирование веб-сервера и Администрирование интернет-сервера
Эти две главы описывают решение проблем, с которыми часто сталкиваются администраторы серверов, имеющих выход в Интернет. Здесь вы найдете два сценария, анализирующие разные аспекты журналирования трафика веб-сервера, инструменты для выявления недействительных внутренних или внешних ссылок, имеющихся на веб-сайте, а также удобный инструмент управления паролями на веб-сервере Apache, упрощающий поддержку файлов .htaccess. Помимо этого исследуются приемы зеркалирования каталогов и целых веб-сайтов.
Глава 11: Сценарии для OS X
OS X, с ее коммерчески успешным и привлекательным графическим интерфейсом, стала огромным шагом вперед в превращении Unix в дружественную операционную систему. Что еще более важно, OS X — это полноценная операционная система Unix, скрытая за симпатичным интерфейсом, а значит, для нее можно написать много полезных и поучительных сценариев. Именно об этом рассказывается в данной главе. В дополнение к инструменту для автоматизации захвата изображения на экране, в этой главе представлены сценарии, помогающие исследовать структуру библиотеки музыкальных произведений iTunes, изменять заголовки окон программы Terminal и усовершенствовать команду open.
Глава 12: Сценарии для игр и забав
Что это за книга о программировании, если в ней не будет хотя бы пары игрушек? Глава 12 объединяет многие идеи и приемы, представленные ранее, и описывает создание шести забавных и довольно сложных игр. Хотя глава написана, чтобы вас развлечь, код каждой игры весьма поучителен. Особенно примечательна игра «Виселица», демонстрирующая некоторые хитрости и необычные приемы программирования сценариев.
Глава 13: Работа в облаке
С момента выхода первого издания этой книги Интернет занимал все больше и больше места в нашей повседневной жизни. Особенно важна для нас тема синхронизации устройств и файлов с облачными службами, такими как iCloud, Dropbox и Google Drive. В главе демонстрируются сценарии командной оболочки, позволяющие в полной мере использовать эти службы и гарантировать своевременную синхронизацию и копирование файлов и каталогов. Кроме того, здесь вы найдете пару сценариев, использующих особенности OS X для работы с фотографиями и озвучивания текста.
Глава 14: ImageMagick и обработка графических файлов
Приложения командной строки могут обрабатывать не только текстовые данные, но и графику. Эта глава посвящена идентификации и обработке изображений из командной строки с использованием комплекта инструментов для работы с графикой, включая открытое программное обеспечение ImageMagick. Сценарии в этой главе реализуют типичные операции с изображениями, от определения их типов до кадрирования и добавления водяных знаков, плюс еще несколько случаев использования.
Глава 15: Дни и даты
Заключительная глава демонстрирует приемы, упрощающие операции с датами и временем: сколько дней разделяют две даты, на какой день недели приходится число или сколько дней осталось до него. Мы решим эти задачи с помощью простых в использовании сценариев командной оболочки.
Приложение A: Установка Bash в Windows 10
Пока мы работали над вторым изданием, компания Microsoft существенно изменила свое отношение к открытому программному обеспечению и в 2016 году даже выпустила полноценную систему bash для Windows 10. Несмотря на то что примеры из книги не тестировались в этой версии bash, многие идеи и решения будет нетрудно перенести в нее. В приложении мы опишем установку bash в Windows 10, чтобы вы могли попробовать свои силы в создании сценариев на компьютере с Windows!
Приложение Б: Дополнительные сценарии
Любой хороший скаут знает, что всегда должен быть запасной план! Работая над этой книгой, мы создавали запасные сценарии на случай, если нам понадобится заменить какой-нибудь из основных. В итоге резервные сценарии нам не потребовались, но с нашей стороны было бы некрасиво держать их в секрете от вас, наших друзей. Это приложение включает три дополнительных сценария: для массового переименования файлов, для массового выполнения команд и для вычисления фаз луны, — которые мы не могли утаить после того, как показали вам 101 сценарий.
Ресурсы в сети
Файлы со всеми сценариями плюс несколько вспомогательных сценариев доступны для загрузки в виде архива на странице: /. В этом же архиве вы найдете несколько файлов ресурсов, которые мы использовали в сценариях, такие как список слов для игры «Виселица» в сценарии № 84 и фрагмент из книги «Алиса в стране чудес», используемый в сценарии № 27.
В заключение
Надеемся, что вам понравится обновленное издание книги и новые сценарии, которые мы добавили в наш классический труд. Увлеченность — неотъемлемая часть обучения, поэтому примеры для книги были подобраны так, чтобы увлечь вас созданием и исследованием сценариев. Мы хотим, чтобы вы получили столько же удовольствия, читая эту книгу, сколько получили мы, работая над ней. Наслаждайтесь!
Глава 0. Краткое введение в сценарии командной оболочки
Bash (как и сценарии на языке командной оболочки в целом) существует уже очень давно, и каждый день новые люди знакомятся с ее возможностями и приемами автоматизации операций с ее применением. И сейчас, когда компания Microsoft выпустила интерактивную оболочку bash и подсистему команд Unix в Windows 10, самое время узнать, насколько простыми и эффективными могут быть сценарии командной оболочки.
Что такое командная оболочка?
С первых дней существования компьютеров сценарии командной оболочки помогали системным администраторам и программистам выполнять рутинную работу, на которую иначе пришлось бы потратить массу времени. Так что же такое «сценарии командной оболочки» и почему они должны волновать вас? Сценарии — это текстовые файлы с набором команд, следующих в порядке их выполнения, на языке конкретной командной оболочки (в нашем случае bash). Командная оболочка (shell) — это интерфейс командной строки к библиотеке команд в операционной системе.
Сценарии командной оболочки по своей сути являются крохотными программами, написанными с использованием команд операционной системы для автоматизации специальных задач — часто таких, выполнение которых вручную не доставляет никакого удовольствия, например, для сбора информации из сети, слежения за использованием дискового пространства, загрузки данных о погоде, переименования файлов и многих других. В виде сценария нетрудно даже реализовать простенькие игры! Такие сценарии могут включать несложную логику, например, инструкции if, которые вы встречали в других языках, но могут быть еще проще, как вы увидите далее.
Многие разновидности командных оболочек, такие как tcsh, zsh и даже популярная оболочка bash, доступны в операционных системах OS X, BSD и Linux. В этой книге основное внимание уделяется главной опоре Unix — командной оболочке bash. Каждая оболочка имеет свои особенности и возможности, но большинство пользователей Unix в первую очередь обычно знакомятся именно с bash. В OS X программа Terminal открывает окно с оболочкой bash (рис. 0.1). В Linux имеется большое разнообразие программ с командной оболочкой, но чаще всего встречаются консоли командной строки: gnome-terminal для GNOME и konsole для KDE. Эти приложения можно настраивать на использование разных типов командных оболочек, но все они по умолчанию используют bash. Фактически в любой Unix-подобной системе, открыв программу-терминал, вы по умолчанию получите доступ к командной оболочке bash.
Рис. 0.1. Вывод версии bash в окне приложения Terminal в OS X
Использование терминала для взаимодействия с операционной системой может показаться сложнейшей задачей. Однако со временем намного естественней становится просто открыть терминал, чтобы быстро изменить что-то в системе, чем перебирать мышью пункты меню, пытаясь отыскать параметры для изменения.
ПРИМЕЧАНИЕ
В августе 2016 года компания Microsoft выпустила версию bash для Windows 10 Anniversary. То есть теперь ее могут запускать пользователи Windows. В приложении A приводятся инструкции по установке bash для Windows 10, но вообще эта книга предполагает, что вы работаете в Unix-подобной системе, такой как OS X или Linux. Вы можете опробовать предлагаемые сценарии в Windows 10, но мы не даем никаких гарантий и сами не тестировали их таким образом! Тем не менее оболочка bash славится своей переносимостью и многие сценарии из этой книги должны работать и в Windows.
Запуск команд
Главная особенность bash — возможность запускать команды в системе. Давайте опробуем короткий пример «Hello World». Команда echo оболочки bash выводит текст на экран, например:
$ echo "Hello World"
Введите данный текст в командной строке bash, и вы увидите, как на экране появятся слова Hello World. Эта строка кода запускает команду echo, хранящуюся в стандартной библиотеке bash. Список каталогов, в которых bash будет искать стандартные команды, хранится в переменной окружения с именем PATH. Вы можете запустить команду echo с переменной PATH, чтобы увидеть ее содержимое, как показано в листинге 0.1.
Листинг 0.1. Вывод текущего содержимого переменной окружения PATH
$ echo $PATH
/Users/bperry/.rvm/gems/ruby-2.1.5/bin:/Users/bperry/.rvm/gems/ruby-2.1.5@global/bin:/Users/bperry/.rvm/rubies/ruby-2.1.5/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/MacGPG2/bin:/Users/bperry/.rvm/bin
ПРИМЕЧАНИЕ
В листингах, где присутствуют вводимые команды и результаты их выполнения, вводимые команды выделены жирным и начинаются с символа $, чтобы вы могли отличить их от вывода, полученного в ходе выполнения команды.
Каталоги в этом выводе отделяются друг от друга двоеточием. Именно их проверит оболочка bash, когда от нее потребуют запустить программу или команду. Если искомая команда хранится в каком-то другом каталоге, bash не сможет запустить ее. Обратите также внимание, что bash проверит перечисленные каталоги именно в том порядке, в каком они перечислены в переменной PATH. Это важно, если у вас имеется две команды с одинаковыми именами, но хранящиеся в разных каталогах, включенных в PATH. Если обнаружится проблема с поиском некоторой команды, попробуйте выполнить команду which с ее именем, как показано в листинге 0.2, чтобы увидеть, в каком каталоге из PATH ее найдет оболочка.
Листинг 0.2. Поиск команд в PATH с помощью which
$ which ruby
/Users/bperry/.rvm/rubies/ruby-2.1.5/bin/ruby
$ which echo
Теперь, вооруженные этой информацией, вы сможете переместить или скопировать файл в один из каталогов, перечисленных командой echo $PATH, как, например, в листинге 0.1, и затем команда начнет запускаться. Мы будем использовать which на протяжении всей книги для определения полного пути к командам. Это удобный инструмент для отладки содержимого переменной PATH.
Настройка оболочки входа
На всем протяжении книги нам предстоит писать сценарии, которые потом будем использовать в других сценариях, поэтому для нас важна простота вызова новых сценариев. Вы можете настроить переменную PATH так, чтобы ваши собственные сценарии вызывались автоматически, как любые другие команды, в момент запуска новой командной оболочки. Когда открывается новый сеанс командной оболочки, она первым делом читает сценарий входа в домашнем каталоге (/Users/<username> в OS X или /home/<username> в Linux) и выполняет любые команды, перечисленные в нем. Сценарий входа называется .login, profile, bashrc или .bash_profile, в зависимости от системы. Чтобы узнать, какой из этих файлов используется как сценарий входа, добавьте в каждый из них следующую строку, заменив последнее слово соответствующим именем файла:
echo this is.profile
Затем выполните вход. Вверху окна терминала должна появиться строка, сообщающая имя файла сценария, выполненного при входе. Если вы откроете терминал и увидите this is.profile, значит, ваша оболочка загружает файл .profile; если вы увидите this is.bashrc, значит, загружается файл .bashrc; и так далее. Однако описанное поведение зависит от типа командной оболочки.
Вы можете добавить в сценарий входа настройку переменной PATH, включив в нее другие каталоги. Здесь же можно подкорректировать любые другие настройки bash, такие как внешний вид строки приглашения к вводу, содержимое переменной PATH и любые другие параметры. Например, воспользуемся командой cat, чтобы заглянуть в измененный сценарий входа .bashrc. Команда cat принимает аргумент с именем файла и выводит его содержимое в окно консоли, как показано в листинге 0.3.
Листинг 0.3. Измененный файл. bashrc, включающий в переменную PATH каталог RVM
$ cat ~/.bashrc
export PATH="$PATH:$HOME/.rvm/bin" # Добавить в PATH каталог RVM для работы
Команда вывела содержимое файла .bashrc, в котором переменной PATH присваивается новое значение, позволяющее локальной версии RVM (Ruby Version Manager — диспетчер версий Ruby) управлять любыми установленными версиями Ruby. Так как сценарий .bashrc настраивает PATH каждый раз, когда открывается новый сеанс работы с командной оболочкой, диспетчер RVM будет доступен по умолчанию.
Аналогично можно открыть доступ к своей библиотеке сценариев командной оболочке. Для этого в своем домашнем каталоге создайте папку, куда будут помещаться разрабатываемые сценарии. Затем добавьте ее в переменную PATH в сценарии входа, чтобы упростить вызов сценариев из нее.
Чтобы выяснить путь к домашнему каталогу, дайте команду echo $HOME, которая выведет в окне терминала полный путь. Перейдите в указанный каталог и создайте папку для разрабатываемых сценариев (мы рекомендуем назвать ее scripts). Затем добавьте эту папку в свой сценарий входа, для чего откройте файл сценария в текстовом редакторе и добавьте в начало файла следующую строку, заменив /path/to/scripts/ на путь к папке с вашими сценариями:
export PATH="/path/to/scripts/:$PATH"
Затем вы сможете запустить любой сценарий из этой папки как обычную команду.
Запуск сценариев командной оболочки
К настоящему моменту мы уже воспользовались некоторыми командами, такими как echo, which и cat. Но мы использовали их по отдельности, а не вместе, то есть не в составе сценария. Давайте напишем сценарий, который выполнит их все последовательно, как показано в листинге 0.4. Этот сценарий выведет Hello World, затем путь к сценарию neqn, который по умолчанию должен быть доступен в оболочке bash. Затем использует этот путь для вывода содержимого сценария neqn на экран. (На данный момент содержимое neqn для нас не важно; мы просто выбрали первый попавшийся сценарий для примера.) Этот пример наглядно демонстрирует использование сценария для выполнения группы команд по порядку, в данном случае, чтобы увидеть полный путь к сценарию и содержимое сценария.
Листинг 0.4. Содержимое нашего первого сценария командной оболочки
echo "Hello World"
echo $(which neqn)
cat $(which neqn)
Откройте текстовый редактор (в Linux, например, большой популярностью пользуются редакторы Vim и gedit, а в OS X — TextEdit) и введите содержимое листинга 0.4. Затем сохраните сценарий с именем intro в своем каталоге для разрабатываемых сценариев. Сценарии командной оболочки не требуют специального расширения файлов, так что сохраните файл с именем без расширения (или, если пожелаете, добавьте расширение .sh, но в этом нет необходимости). Первая строка в сценарии вызывает команду echo, чтобы просто вывести текст Hello World. Вторая строка чуть сложнее; она использует команду which для поиска файла сценария neqn и затем с помощью echo выводит найденный путь на экран. Чтобы выполнить такую связку команд, где одна передается другой в виде аргумента, bash использует подоболочку, в которой выполняет вторую команду и сохраняет ее вывод для передачи первой. В нашем примере подоболочка выполнит команду which, которая вернет полный путь к сценарию neqn. Затем этот путь будет передан как аргумент команде echo, которая просто выведет его на экран. Наконец, тот же трюк с подоболочкой используется для передачи пути к сценарию neqn команде cat, которая выведет содержимое сценария neqn.
Сохраните файл и запустите сценарий в окне терминала. Вы должны увидеть результат, показанный в листинге 0.5.
Листинг 0.5. Результат запуска нашего первого сценария командной оболочки
··$ sh intro
··Hello World
··/usr/bin/neqn
··#!/bin/sh
··# Присутствие этого сценария не должно расцениваться как наличие поддержки
··#·· GNU eqn и groff −Tascii|-Tlatin1 |-Tutf8 |-Tcp1047
··GROFF_RUNTIME="${GROFF_BIN_PATH=/usr/bin}:"
··PATH="$GROFF_RUNTIME$PATH"
··export PATH
··exec eqn −Tascii ${1+"$@"}
Запуск сценария производится с помощью команды sh, которой имя сценария intro передается как аргумент. Команда sh обойдет все строки в файле и выполнит их, как если бы это были команды bash, введенные в окне терминала. Как показано в листинге 0.5, сначала на экран выводится строка Hello World , затем путь к файлу neqn . В заключение выводится содержимое файла neqn ; это исходный код короткого сценария командной оболочки neqn, хранящегося на вашем жестком диске (в OS X, по крайней мере, в Linux содержимое этого сценария может немного отличаться).
Упрощение способа вызова сценариев
Для запуска сценариев не обязательно использовать команду sh. Если добавить еще одну строку в сценарий intro и изменить его разрешения в файловой системе, его можно будет запускать непосредственно, без команды sh, как любые другие команды. Откройте сценарий intro в текстовом редакторе и измените его, как показано ниже:
··#!/bin/bash
··echo "Hello World"
··echo $(which neqn)
··cat $(which neqn)
Мы добавили единственную строку в самое начало файла, ссылающуюся на путь в файловой системе /bin/bash . Эта строка называется shebang[1]. С ее помощью командная оболочка определяет, какую программу запустить для интерпретации сценария. Здесь в качестве интерпретатора мы указали bash. Вы можете встретить другие строки shebang, например, в сценариях на языке Perl (#!/usr/bin/perl) или Ruby (#!/usr/bin/env ruby).
После добавления строки нам еще необходимо установить права доступа к файлу, разрешающие выполнять его как обычную программу. Для этого в окне терминала выполните команды, показанные в листинге 0.6.
Листинг 0.6. Изменение прав доступа к файлу сценария intro, разрешающих его выполнение
··$ chmod +x intro
··$./intro
··Hello World
··/usr/bin/neqn
··#!/bin/sh
··# Присутствие этого сценария не должно расцениваться как наличие поддержки
··# GNU eqn и groff −Tascii|-Tlatin1 |-Tutf8 |-Tcp1047
··GROFF_RUNTIME="${GROFF_BIN_PATH=/usr/bin}:"
··PATH="$GROFF_RUNTIME$PATH"
··export PATH
··exec eqn −Tascii ${1+"$@"}
Для изменения прав доступа мы использовали команду chmod и передали ей аргумент +x, который требует от команды дать указанному файлу право на выполнение, и имя самого файла. После настройки права на выполнение для сценария, чтобы запускать его как обычную программу, мы можем вызвать сценарий непосредственно, как показано в строке , без вызова самой оболочки bash. Это общепринятая практика в разработке сценариев командной оболочки, и вы со временем поймете ее полезность. Большинству сценариев, которые мы напишем в этой книге, так же потребуется дать право на выполнение, подобно сценарию intro.
Мы привели лишь простой пример, чтобы показать, как запускать сценарии командной оболочки и как использовать сценарии для запуска других сценариев. Во многих сценариях в этой книге мы задействуем именно такой метод, и вы еще не раз увидите строки shebang в будущем.
Почему именно сценарии командной оболочки?
Кого-то из вас может беспокоить вопрос: почему для создания сценариев предпочтительнее использовать язык командной оболочки bash вместо более новых и мощных языков, таких как Ruby и Go. Да, эти языки гарантируют переносимость между разными типами систем, но они не устанавливаются по умолчанию. Причина проста: на любой машине с операционной системой Unix имеется командная оболочка, и на подавляющем большинстве из них используется оболочка bash. Как отмечалось в начале главы, компания Microsoft недавно выпустила для Windows 10 ту же самую командную оболочку bash, которая имеется во всех основных дистрибутивах Linux и OS X. То есть теперь сценарии командной оболочки стали еще более переносимыми с минимумом усилий с вашей стороны. Кроме того, сценарии на языке командной оболочки позволяют быстрее и проще решать задачи обслуживания и администрирования системы, чем сценарии на других языках. Оболочка bash все еще далека от идеала, но в этой книге вы узнаете, как смягчить некоторые ее недостатки.
В листинге 0.7 приводится пример маленького, удобного и полностью переносимого сценария командной оболочки (фактически, это однострочная команда на bash!). Сценарий определяет общее количество страниц во всех документах OpenOffice, находящихся в указанной папке, и может пригодиться писателям.
Листинг 0.7. Сценарий для определения общего количества страниц во всех документах OpenOffice в указанной папке
#!/bin/bash
echo "$(exiftool *.odt | grep Page-count | cut −d":" −f2 | tr 'n' '+')""0" | bc
Не будем обсуждать тонкости работы этого сценария — в конце концов, мы только в самом начале пути. Но в общих чертах отметим, что он извлекает информацию о количестве страниц из каждого документа, выстраивает строку из полученных чисел, перемежая их операторами сложения, и передает ее калькулятору командной строки для вычисления суммы. На все про все оказалось достаточно одной строки кода. В книге вы найдете еще множество таких же потрясающих сценариев, как этот, и после некоторой практики он покажется вам невероятно простым!
Теперь вы должны представлять, как создаются сценарии командной оболочки, если прежде вы этим не занимались. Создание коротких сценариев для решения специализированных задач заложено в основу философии Unix. Умение писать собственные сценарии и расширять возможности системы Unix под свои потребности даст вам огромную власть. Эта глава лишь намекнула, что ждет вас впереди: множество по-настоящему потрясающих сценариев командной оболочки!
Глава 1. Отсутствующая библиотека
Одна из замечательных особенностей Unix — возможность создавать новые команды, объединяя старые новыми способами. Но даже при том, что Unix включает сотни команд и предоставляет тысячи способов их комбинирования, вы все еще можете столкнуться с ситуацией, когда никакая из комбинаций не позволит решить поставленную задачу правильно. В этой главе мы исследуем основные аспекты, знание которых поможет вам создавать более сложные и интеллектуальные программы на языке командной оболочки.
Но есть еще кое-что, о чем необходимо поговорить в самом начале: среда программирования на языке командной оболочки не так сложна, как другие среды программирования на настоящих языках. Perl, Python, Ruby и даже C имеют структуры и библиотеки, предлагающие дополнительные возможности, тогда как сценарии на языке командной оболочки — это в большей степени ваш собственный мир. Сценарии в данной главе помогут вам найти в нем свой путь. Далее они послужат строительными блоками для создания более мощных сценариев.
Наибольшую сложность при разработке сценариев представляют также тонкие различия между разновидностями Unix и дистрибутивами GNU/Linux. Даже при том, что стандарты IEEE POSIX определяют общую функциональную основу для всех реализаций Unix, иногда все же бывает непросто начать пользоваться системой OS X после нескольких лет работы в окружении Red Hat GNU/Linux. Команды различаются, хранятся в разных каталогах и часто имеют тонкие различия в интерпретации флагов. Эти различия могут сделать создание сценариев командной оболочки непростым занятием, но мы познакомим вас с некоторыми хитростями, помогающими справляться с этими сложностями.
Что такое POSIX?
В первые дни Unix был сродни Дикому Западу: разные компании создавали новые версии операционной системы и развивали их в разных направлениях, одновременно уверяя клиентов, что все эти новые версии — просто разновидности Unix, совместимые между собой. Но в дело вмешался Институт инженеров электротехники и электроники (Institute for Electrical and Electronic Engineers, IEEE) и, объединив усилия всех основных производителей, разработал стандартное определение Unix под названием «Интерфейс переносимой операционной системы» (Portable Operating System Interface, или POSIX), которому должны были соответствовать все коммерческие и открытые реализации Unix. Нельзя купить операционную систему POSIX как таковую, но все доступные версии Unix и GNU/Linux в общих чертах соответствуют требованиям POSIX (хотя некоторые ставят под сомнение необходимость стандарта POSIX, когда GNU/Linux сам стал стандартом де-факто).
Однако иногда даже POSIX-совместимые реализации Unix отличаются друг от друга. В качестве примера можно привести команду echo, о которой рассказывается далее в этой главе. Отдельные версии этой команды поддерживают флаг −n, который запрещает добавлять символ перевода строки по умолчанию. Другие версии echo поддерживают экранированную последовательность c, которая интерпретируется как «не включать перевод строки», а третьи вообще не дают возможности запретить добавление этого символа в конце вывода. Более того, отдельные системы Unix имеют командные оболочки, где команда echo реализована как встроенная функция, которая игнорирует флаги −n и c, а также включают стандартную реализацию команды в виде двоичного файла /bin/echo, обрабатывающую эти флаги. В результате возникают сложности со сценариями запросов на ввод данных, потому что сценарии должны работать одинаково в как можно большем количестве версий Unix. Следовательно, для нормальной работы сценариев важно нормализовать поведение команды echo, чтобы оно было единообразным в разных системах. Далее в этой главе, в сценарии № 8, вы увидите, как заключить команду echo в сценарий командной оболочки, чтобы получить такую нормализованную версию.
ПРИМЕЧАНИЕ
Некоторые сценарии в этой книге используют дополнительные возможности bash, поддерживаемые не всеми POSIX-совместимыми командными оболочками.
Но хватит теории — приступим к знакомству со сценариями, которые будут включены в нашу библиотеку!
№ 1. Поиск программ в PATH
Сценарии, использующие переменные окружения (такие как MAILER или PAGER), таят в себе скрытую опасность: некоторые их настройки могут ссылаться на несуществующие программы. Для тех, кто не сталкивался прежде с этими переменными окружения, отметим, что MAILER должна хранить путь к программе электронной почты (например, /usr/bin/mailx), а PAGER должна ссылаться на программу постраничного просмотра длинных документов. Например, если вы решите увеличить гибкость сценария и вместо системной программы постраничного просмотра по умолчанию (обычно more или less) использовать для отображения вывода сценария переменную PAGER, необходимо убедиться, что эта переменная содержит действительный путь к существующей программе.
Этот первый сценарий показывает, как проверить доступность указанной программы в списке путей PATH. Он также послужит отличной демонстрацией нескольких приемов программирования на языке командной оболочки, включая определение функций и переменных. Листинг 1.1 показывает, как проверить допустимость путей к файлам.
Листинг 1.1. Сценарий inpath с определениями функций
#!/bin/bash
# inpath — Проверяет допустимость пути к указанной программе
# или ее доступность в каталогах из списка PATH
··# Получает команду и путь, пытается отыскать команду. Возвращает 0, если
··#·· команда найдена и является выполняемым файлом; 1 — если нет. Обратите
··#·· внимание, что эта функция временно изменяет переменную окружения
··#·· IFS (Internal Field Separator — внутренний разделитель полей), но
··#·· восстанавливает ее перед завершением.
··cmd=$1········ourpath=$2···· result=1
··oldIFS=$IFS·· IFS=":"
··for directory in "$ourpath"
····if [-x $directory/$cmd]; then
······result=0······ # Если мы здесь, значит, команда найдена.
··IFS=$oldIFS
··return $result
checkForCmdInPath()
··if ["$var"!= ""]; then
····if ["${var:0:1}" = "/"]; then
······if [! -x $var]; then
········return 1
····elif! in_path $var "$PATH"; then
······return 2
В главе 0 мы рекомендовали создать в своем домашнем каталоге новую папку scripts и добавить полный путь к ней в свою переменную окружения PATH. Выполните команду echo $PATH, чтобы увидеть текущее значение переменной PATH, и добавьте в сценарий входа (.login, profile, bashrc или .bash_profile, в зависимости от оболочки) строку, изменяющую значение PATH. Подробности ищите в разделе «Настройка оболочки входа» в главе 0.
ПРИМЕЧАНИЕ
Если попробовать вывести список файлов в каталоге с помощью команды ls, некоторые специальные файлы, такие как .bashrc и .bash_profile, могут не отображаться. Это объясняется тем, что файлы, имена которых начинаются с точки, например .bashrc, считаются «скрытыми». (Как оказывается, эта «ошибка, превратившаяся в «фишку» была допущена еще в самом начале развития Unix.) Чтобы вывести все файлы, включая скрытые, добавьте в команду ls флаг −a.
Напомним еще раз: все наши сценарии написаны в предположении, что они будут выполняться командной оболочкой bash. Обратите внимание: этот сценарий явно указывает в первой строке (называется shebang), что для его интерпретации должен использоваться интерпретатор /bin/bash. Многие системы поддерживают также строку shebang /usr/bin/env bash, которая определяет местонахождение интерпретатора в момент запуска сценария.
ЗАМЕЧАНИЕ О КОММЕНТАРИЯХ
Мы долго думали, включать ли в код подробное описание работы сценария, и решили, что в некоторых случаях будем приводить пояснения к особенно заковыристым фрагментам после самого кода, но в общем случае для пояснения происходящего будем использовать комментарии в коде. Ищите строки, начинающиеся с символа #, или текст в строках кода, которому предшествует символ #.
Поскольку вам придется читать сценарии других людей (не только наши!), будет полезно попрактиковаться понимать происходящее в сценариях по комментариям в них. Кроме того, писать комментарии — хорошая привычка, которую желательно выработать у себя при работе над собственными сценариями, потому что это поможет вам понять, чего вы стремитесь достигнуть в разных блоках кода.
Как это работает
Функция checkForCmdInPath отличает значение параметра с одним только именем программы (например, echo) от значения, содержащего полный путь, плюс имя файла (например, /bin/echo). Для этого она сравнивает первый символ в переданном ей значении с символом /; для чего ей требуется изолировать первый символ от остального значения параметра.
Обратите внимание на синтаксис ${var:0:1} — это сокращенная форма извлечения подстроки: указывается начальная позиция в исходной строке и длина извлекаемой подстроки (если длина не указана, возвращается остаток строки до конца). Выражение ${var:10}, например, вернет остаток строки в $var начиная с десятого символа, а ${var:10:6} вернет только символы, заключенные между позициями 10 и 15 включительно. Что это означает, демонстрирует следующий пример:
$ var="something wicked this way comes…"
$ echo ${var:10}
wicked this way comes…
$ echo ${var:10:6}
В листинге 1.1 данный синтаксис используется, чтобы определить, начинается ли указанный путь с символа слеша. Если это так, то далее функция проверяет наличие указанного файла в файловой системе по указанному пути. Пути, начинающиеся с символа /, являются абсолютными, и для их проверки можно использовать оператор −x . В противном случае значение параметра передается в функцию inpath , чтобы проверить наличие указанного файла в одном из каталогов, перечисленных в PATH.
Запуск сценария
Чтобы запустить сценарий как самостоятельную программу, нужно добавить в самый конец файла короткий блок команд. Эти команды просто принимают ввод пользователя и передают его в функцию, как показано ниже.
if [$# −ne 1]; then
··echo "Usage: $0 command" >&2
checkForCmdInPath "$1"
case $? in
··0) echo "$1 found in PATH";;
··1) echo "$1 not found or not executable";;
··2) echo "$1 not found in PATH";;
После добавления кода сценарий можно запустить непосредственно, как показано далее, в разделе «Результаты». Закончив эксперименты со сценарием, не забудьте удалить или закомментировать дополнительный код, чтобы потом его можно было подключать как библиотеку функций.
Результаты
Для проверки вызовем сценарий inpath с именами трех программ: существующей программы, также существующей программы, но находящейся в каталоге, не включенном в список PATH, и несуществующей программы, но с полным путем к ней. Пример тестирования сценария приводится в листинге 1.2.
Листинг 1.2. Тестирование сценария inpath
$ inpath echo
echo found in PATH
$ inpath MrEcho
MrEcho not found in PATH
$ inpath /usr/bin/MrEcho
/usr/bin/MrEcho not found or not executable
Последний блок кода, добавленный позднее, преобразует результат вызова функции in_path в нечто более читаемое, поэтому теперь мы легко можем видеть, что все три случая обрабатываются, как ожидалось.
Усовершенствование сценария
Для желающих начать овладевать мастерством программирования с первого сценария, покажем, как заменить выражение ${var:0:1} его более сложной формой: ${var%${var#?}}. Такой метод извлечения подстрок определяет стандарт POSIX. Эта галиматья в действительности включает два выражения извлечения подстроки. Внутреннее выражение ${var#?} извлекает из var все, кроме первого символа, где # удаляет первое совпадение с заданным шаблоном, а? — это регулярное выражение, которому соответствует точно один символ.
Внешнее выражение ${var%pattern} возвращает подстроку из строки слева, оставшуюся после удаления указанного шаблона pattern из var. В данном случае удаляемый шаблон pattern — это результат внутреннего выражения, то есть внешнее выражение вернет первый символ в строке.
Для тех, кому POSIX-совместимый синтаксис кажется пугающим, отметим, что большинство командных оболочек (включая bash, ksh и zsh) поддерживает другой метод извлечения подстрок, ${varname: start: size}, который был использован в сценарии.
Те, кому не нравится ни один из представленных способов извлечения первого символа, могут использовать системные команды: $(echo $var | cut −c1). В программировании на bash практически любую задачу, будь то извлечение, преобразование или загрузка данных из системы, можно решить несколькими способами. При этом важно понимать, что наличие нескольких способов не означает, что один способ лучше другого.
Кроме того, чтобы сценарий различал, запускается он как самостоятельная программа или подключается другим сценарием, можно добавить в начало условный оператор:
if ["$BASH_SOURCE" = "$0"]
Это сработает и с любым другим сценарием. Однако мы предлагаем вам, дорогой читатель, дописать остальной код после экспериментов!
ПРИМЕЧАНИЕ
Сценарий № 47 в главе 6 тесно связан с этим сценарием. Он проверяет каталоги в PATH и переменные в окружении пользователя.
№ 2. Проверка ввода: только алфавитно-цифровые символы
Пользователи постоянно игнорируют указания и вводят недопустимые данные, в неправильном формате или неправильным синтаксисом. Как разработчик сценариев командной оболочки вы должны обнаружить и отметить такие ошибки еще до того, как они превратятся в проблемы.
Часто подобные ситуации связаны с вводом имен файлов или ключей в базе данных. Программа просит пользователя ввести строку, которая должна содержать только алфавитно-цифровые символы, то есть только буквы верхнего или нижнего регистра и цифры — никаких знаков пунктуации, специальных символов и пробелов. Правильную ли строку ввел пользователь? Ответ на этот вопрос дает сценарий в листинге 1.3.
Листинг 1.3. Сценарий validalnum
#!/bin/bash
# validAlphaNum — проверяет, содержит ли строка только
# алфавитные и цифровые символы
validAlphaNum()
··# Проверка аргумента: возвращает 0, если все символы в строке являются
··#·· буквами верхнего/нижнего регистра или цифрами; иначе возвращает 1
··# Удалить все недопустимые символы.
··validchars="$(echo $1 | sed −e 's/[^[: alnum: ]]//g’)"
··if ["$validchars" = "$1"]; then
····return 0
····return 1
# НАЧАЛО ОСНОВНОГО СЦЕНАРИЯ — УДАЛИТЕ ИЛИ ЗАКОММЕНТИРУЙТЕ ВСЕ, ЧТО НИЖЕ,
# ЧТОБЫ ЭТОТ СЦЕНАРИЙ МОЖНО БЫЛО ПОДКЛЮЧАТЬ К ДРУГИМ СЦЕНАРИЯМ.
# =================
/bin/echo −n "Enter input: "
read input
# Проверка ввода
if! validAlphaNum "$input"; then
··echo "Please enter only letters and numbers." >&2
··echo "Input is valid."
Как это работает
Логика работы сценария проста: сначала с помощью редактора sed создается новая версия введенных данных, из которой удалены все недопустимые символы . Затем новая версия сравнивается с оригиналом . Если две версии оказались одинаковыми, все в порядке. В противном случае, если в результате обработки редактором sed потерялись данные, значит, исходная версия содержит недопустимые символы.
В основе работы сценария лежит операция подстановки редактора sed, которая удаляет любые символы, не входящие в множество [: alnum: ], где [: alnum: ] — это сокращение POSIX для регулярного выражения, соответствующего всем алфавитно-цифровым символам. Если результат операции подстановки не совпадает с исходным вводом, значит, в исходной строке присутствуют другие символы, кроме алфавитно-цифровых, недопустимые в данном случае. Функция возвращает ненулевое значение, чтобы сообщить о проблеме. Имейте в виду: в этом примере предполагается, что введенные данные являются текстом ASCII.
Запуск сценария
Сценарий содержит все необходимое для его запуска как самостоятельной программы. Он предлагает ввести строку и затем сообщает о ее допустимости. Однако чаще эта функция используется для копирования в начало другого сценария в виде ссылки, как показано в сценарии № 12.
Сценарий validalnum также представляет собой хороший пример программирования на языке командной оболочки вообще: сначала пишутся функции, а затем они тестируются перед включением в другие, более сложные сценарии. Такой подход позволяет избавиться от многих неприятностей.
Результаты
Сценарий validalnum прост в применении, он предлагает пользователю ввести строку для проверки. В листинге 1.4 показано, как сценарий реагирует на допустимый и недопустимый ввод.
Листинг 1.4. Тестирование сценария validalnum
$ validalnum
Enter input: valid123SAMPLE
Input is valid.
$ validalnum
Enter input: this is most assuredly NOT valid, 12345
Please enter only letters and numbers.
Усовершенствование сценария
Метод «удалить недопустимые символы и посмотреть, что осталось» хорошо подходит для проверки благодаря своей гибкости. При этом важно помнить, что обе переменные — исходная строка и шаблон — должны заключаться в двойные кавычки, чтобы избежать ошибок в случае ввода пустой строки (или пустого шаблона). Пустые значения переменных — извечная проблема в программировании сценариев, потому что при проверке в условном операторе они вызывают сообщение об ошибке. Всегда помните, что пустая строка в кавычках отличается от пустого значения переменной.
Хотите потребовать, чтобы ввод содержал только буквы верхнего регистра, пробелы, запятые и точки? Просто измените шаблон подстановки в строке , как показано ниже:
sed 's/[^[: upper: ],]//g'
Эту же функцию можно использовать для простейшей проверки телефонных номеров (допускается присутствие цифр, пробелов, круглых скобок и дефисов, но не допускается наличие пробелов в начале или нескольких пробелов, идущих подряд), если использовать шаблон:
sed 's/[^- [: digit: ]()]//g'
Но, если нужно ограничить ввод целыми числами, опасайтесь ловушки. Например, на первый взгляд кажется, что следующий шаблон справится с этой задачей:
sed 's/[^[: digit: ]]//g'
Однако он будет пропускать только положительные целые числа. А что, если вам необходимо разрешить ввод отрицательных чисел? Если вы просто добавите знак «минус» в множество допустимых символов, функция признает допустимой строку -3–4, хотя совершенно очевидно, что она не является допустимым целым числом. Обработка отрицательных чисел демонстрируется в сценарии № 5.
№ 3. Нормализация форматов дат
Разработчикам сценариев часто приходится иметь дело с большим количеством разнообразных форматов представления дат, нормализация которых может быть сопряжена с разными сложностями. Самые серьезные проблемы связаны с датами, потому что они записываются самыми разными способами. Даже если потребовать ввести дату в определенном формате, например месяц-день-год, вы почти наверняка получите несовместимый ввод: номер месяца вместо названия, сокращенное название вместо полного или даже полное название со всеми буквами в верхнем регистре. По этой причине функция нормализации дат, даже самая простенькая, послужит очень хорошим строительным блоком для многих сценариев, особенно таких, как сценарий № 7.
Сценарий в листинге 1.5 нормализует строки с датами, используя относительно простой набор критериев: месяц должен задаваться именем или числом в диапазоне от 1 до 12, а год — четырехзначным числом. Нормализованная строка с датой включает название месяца (в виде трехсимвольного сокращения), за которым следуют день месяца и четырехзначный год.
Листинг 1.5. Сценарий normdate
··#!/bin/bash
··# normdate — Нормализует поле месяца в строке с датой в трехсимвольное
··#·· представление, с первой буквой в верхнем регистре.
··#·· Вспомогательная функция для сценария № 7, valid-date.
··#·· В случае успеха возвращает 0.
··monthNumToName()
····# Присвоить переменной 'month’ соответствующее значение.
····case $1 in
······1) month="Jan";; 2) month="Feb";;
······3) month="Mar";; 4) month="Apr";;
······5) month="May";; 6) month="Jun";;
······7) month="Jul";; 8) month="Aug";;
······9) month="Sep";; 10) month="Oct";;
······11) month="Nov";; 12) month="Dec";;
······*) echo "$0: Unknown month value $1" >&2
········exit 1
····return 0
··# НАЧАЛО ОСНОВНОГО СЦЕНАРИЯ — УДАЛИТЕ ИЛИ ЗАКОММЕНТИРУЙТЕ ВСЕ, ЧТО НИЖЕ,
··# ЧТОБЫ ЭТОТ СЦЕНАРИЙ МОЖНО БЫЛО ПОДКЛЮЧАТЬ К ДРУГИМ СЦЕНАРИЯМ.
··# =================
··# Проверка ввода
··if [$# −ne 3]; then
····echo "Usage: $0 month day year" >&2
····echo "Formats are August 3 1962 and 8 3 1962" >&2
····exit 1
··if [$3 −le 99]; then
····echo "$0: expected 4-digit year value." >&2
····exit 1
··# Месяц введен как число?
··if [-z $(echo $1|sed 's/[[: digit: ]]//g')]; then
····monthNumToName $1
··# Нормализовать до 3 первых букв, первая в верхнем регистре, остальные в нижнем.
····month="$(echo $1|cut −c1|tr '[: lower: ]' '[: upper: ]')"
····month="$month$(echo $1|cut −c2-3 | tr '[: upper: ]' '[: lower: ]')"
··echo $month $2 $3
Как это работает
Обратите внимание на третий условный оператор в этом сценарии . Он выбрасывает из поля с месяцем все цифры и затем с помощью оператора −z проверяет, получилась ли в результате пустая строка. Если получилась, это означает, что в поле содержатся только цифры, соответственно, его можно напрямую преобразовать в название месяца вызовом функции monthNumToName, которая дополнительно проверяет номер месяца на попадание в диапазон от 1 до 12. Иначе предполагается, что первое поле во введенной строке содержит название месяца, которое нормализуется сложной последовательностью команд cut и tr с использованием двух подоболочек (то есть последовательности команд заключены в скобки $(и), которые вызывают заключенные в них команды и возвращают их вывод).
Первая последовательность команд в подоболочке, в строке , извлекает первый символ из поля с названием месяца и с помощью tr преобразует его в верхний регистр (последовательность echo $1|cut −c1 можно также записать в стиле POSIX: ${1 %${1#?}}, как было показано выше). Вторая последовательность, в строке , извлекает второй и третий символы и преобразует их в нижний регистр. В результате получается трехсимвольное сокращенное название месяца с первым символом в верхнем регистре. Обратите внимание, что в данном случае не проверяется — содержит ли исходное поле допустимое название месяца, в отличие от случая, когда месяц задается числом.
Запуск сценария
Для максимальной гибкости будущих сценариев, использующих normdate, этот сценарий спроектирован так, что принимает исходные данные в виде трех аргументов командной строки, как показано в листинге 1.6. Если вы предполагаете использовать сценарий только интерактивно, предложите пользователю ввести дату в виде трех значений, однако это усложнит вызов normdate из других сценариев.
Результаты
Листинг 1.6. Тестирование сценария normdate
$ normdate 8 3 62
normdate: expected 4-digit year value.
$ normdate 8 3 1962
Aug 3 1962
$ normdate AUGUST 03 1962
Aug 03 1962
Обратите внимание, что этот сценарий нормализует только представление месяца; представление дня (в том числе с ведущими нулями) и года не изменяется.
Усовершенствование сценария
Прежде чем знакомиться с разными усовершенствованиями, которые можно добавить в этот сценарий, загляните в раздел с описанием сценария № 7, где используется normdate для проверки вводимых дат.
Одно из изменений, которые можно внедрить уже сейчас, касается включения поддержки дат в форматах MM/DD/YYYY и MM-DD-YYYY, для чего достаточно добавить следующий код непосредственно перед первым условным оператором:
if [$# −eq 1]; then # Чтобы компенсировать форматы с / и -
··set −$(echo $1 | sed 's/[/-]/ /g')
С этим изменением сценарий позволяет вводить и нормализовать даты в следующих распространенных форматах:
$ normdate 6-10-2000
Jun 10 2000
$ normdate March-11-1911
Mar 11 1911
$ normdate 8/3/1962
Aug 3 1962
Если вы прочитаете код очень внимательно, то заметите, что в нем можно также усовершенствовать проверку поля с номером года, не говоря уже о поддержке разных международных форматов представления дат. Мы оставляем это вам как упражнение для самостоятельных исследований!
№ 4. Удобочитаемое представление больших чисел
Программисты часто допускают типичную ошибку, отображая результаты вычислений без предварительного форматирования. Пользователям сложно определить, например, сколько миллионов содержится в числе 43 245 435, не подсчитав количество цифр справа налево и не добавив мысленно запятые после каждого третьего знака. Сценарий в листинге 1.7 выводит большие числа в удобочитаемом формате.
Листинг 1.7. Сценарий nicenumber форматирует большие числа, делая их удобочитаемыми
··#!/bin/bash
··# nicenumber — Отображает переданное число в формате представления с запятыми.
··#·· Предполагает наличие переменных DD (decimal point delimiter — разделитель
··#·· дробной части) и TD (thousands delimiter — разделитель групп разрядов).
··#·· Создает переменную nicenum с результатом, а при наличии второго аргумента
··#·· дополнительно выводит результат в стандартный вывод.
··nicenumber()
····# Обратите внимание: предполагается, что для разделения дробной и целой
····#·· части во входном значении используется точка.
····#·· В выходной строке в качестве такого разделителя используется точка, если
····#·· пользователь не определил другой символ с помощью флага −d.
····integer=$(echo $1 | cut −d. -f1) # Слева от точки
····decimal=$(echo $1 | cut −d. -f2) # Справа от точки
····# Проверить присутствие дробной части в числе.
····if ["$decimal"!= "$1"]; then
······# Дробная часть есть, включить ее в результат.
······result="${DD:= '.'}$decimal"
··thousands=$integer
··while [$thousands −gt 999]; do
····remainder=$(($thousands % 1000)) # Три последние значимые цифры
····# В 'remainder' должно быть три цифры. Требуется добавить ведущие нули?
····while [${#remainder} −lt 3]; do # Добавить ведущие нули
······remainder="0$remainder"
····result="${TD:=","}${remainder}${result}" # Конструировать справа налево
····thousands=$(($thousands / 1000)) # Оставить остаток, если есть
··nicenum="${thousands}${result}"
··if [! -z $2]; then
····echo $nicenum
DD="." # Десятичная точка для разделения целой и дробной части
TD="," # Разделитель групп разрядов
# Начало основного сценария
# =================
··while getopts "d: t: " opt; do
····case $opt in
······d) DD="$OPTARG";;
······t) TD="$OPTARG";;
··shift $(($OPTIND — 1))
··# Проверка ввода
··if [$# −eq 0]; then
····echo "Usage: $(basename $0) [-d c] [-t c] number"
····echo " −d specifies the decimal point delimiter"
····echo " −t specifies the thousands delimiter"
····exit 0
··nicenumber $1 1 # Второй аргумент заставляет nicenumber вывести результат.
Как это работает
Основная работа в этом сценарии выполняется циклом while внутри функции nicenumber() , который последовательно удаляет три младших значащих разряда из числового значения в переменной thousands и присоединяет их к создаваемой форматированной версии числа . Затем цикл уменьшает числовое значение в thousands и повторяет итерацию, если необходимо. Вслед за функцией nicenumber() начинается основная логика сценария. Сначала с помощью getopts , анализируются параметры, переданные в сценарий, и затем вызывается функция nicenumber() с последним аргументом, указанным пользователем.
Запуск сценария
Чтобы опробовать этот сценарий, просто вызовите его с очень большим числом. Сценарий добавит десятичную точку и разделители групп разрядов, использовав значения либо по умолчанию, либо указанные с помощью флагов.
Результат можно внедрить в сообщение, как показано ниже:
echo "Do you really want to pay $$(nicenumber $price)?"
Результаты
Сценарий nicenumber может также принимать дополнительные параметры. Листинг 1.8 демонстрирует форматирование нескольких чисел с использованием сценария.
Листинг 1.8: Тестирование сценария nicenumber
$ nicenumber 5894625
$ nicenumber 589462532.433
589,462,532.433
$ nicenumber −d, -t. 589462532.433
589.462.532,433
Усовершенствование сценария
В разных странах используют разные символы в качестве десятичной точки и для разделения групп разрядов, поэтому в сценарии предусмотрена возможность передачи дополнительных флагов. Например, в Германии и Италии сценарию следует передать −d"." и −t",", во Франции −d"," и −t " ", а в Швейцарии, где четыре государственных языка, следует использовать −d"." и −t "'". Это отличный пример ситуации, когда гибкость оказывается ценнее жестко определенных значений, потому что инструмент становится полезным для более широкого круга пользователей.
С другой стороны, мы жестко установили, что во входных значениях роль десятичной точки будет играть символ".", то есть, если вы предполагаете использование другого разделителя дробной и целой части во входных значениях, измените символ в двух вызовах команды cut в строках и , где сейчас используется".".
Ниже показано одно из решений:
integer=$(echo $1 | cut −d$DD −f1) # Слева от точки
decimal=$(echo $1 | cut −d$DD −f2) # Справа от точки
Это решение работоспособно, только если разделитель дробной и целой части во входном значении не отличается от разделителя, выбранного для результата, в противном случае сценарий просто не будет работать. Более сложное решение состоит в том, чтобы непосредственно перед этими двумя строками включить проверку, позволяющую убедиться, что разделитель дробной и целой части во входном значении совпадает с разделителем, указанным пользователем. Для реализации проверки можно использовать тот же трюк, что был показан в сценарии № 2: отбросить все цифры и посмотреть, что осталось, например:
separator="$(echo $1 | sed 's/[[: digit: ]]//g')"
if [! -z "$separator" −a "$separator"!= "$DD"]; then
··echo "$0: Unknown decimal separator $separator encountered." >&2
№ 5. Проверка ввода: целые числа
Как было показано в сценарии № 2, проверка целых чисел осуществляется очень просто, пока дело не доходит до отрицательных значений. Проблема в том, что всякое отрицательное число может содержать только один знак «минус», который обязан быть первым. Процедура проверки в листинге 1.9 оценивает правильность форматирования отрицательных чисел и, что особенно ценно, может проверить вхождение значений в установленный пользователем диапазон.
Листинг 1.9. Сценарий validint
#!/bin/bash
# validint — Проверяет целые числа, поддерживает отрицательные значения
validint()
··# Проверяет первое значение и сравнивает с минимальным значением $2 и/или
··#·· с максимальным значением $3, если они заданы. Если проверяемое значение
··#·· вне заданного диапазона или не является допустимым целым числом,
··#·· возвращается признак ошибки.
··number="$1"; min="$2"; max="$3"
··if [-z $number]; then
····echo "You didn't enter anything. Please enter a number." >&2
····return 1
··# Первый символ — знак "минус"?
··if ["${number%${number#?}}" = "-"]; then
····testvalue="${number#?}" # Оставить для проверки все, кроме первого символа
····testvalue="$number"
··# Удалить все цифры из числа для проверки.
··nodigits="$(echo $testvalue | sed 's/[[: digit: ]]//g')"
··# Проверить наличие нецифровых символов.
··if [! -z $nodigits]; then
····echo "Invalid number format! Only digits, no commas, spaces, etc." >&2
····return 1
··if [! -z $min]; then
····# Входное значение меньше минимального?
····if ["$number" −lt "$min"]; then
······echo "Your value is too small: smallest acceptable value is $min." >&2
······return 1
··if [! -z $max]; then
····# Входное значение больше максимального?
····if ["$number" −gt "$max"]; then
······echo "Your value is too big: largest acceptable value is $max." >&2
······return 1
··return 0
Как это работает
Проверка целочисленных значений реализуется очень просто благодаря тому что такие значения состоят исключительно из последовательности цифр (от 0 до 9), перед которой может находиться единственный знак «минус». Если в вызов функции validint() передать минимальное и (или) максимальное значение, она также проверит вхождение заданного значения в указанный диапазон.
Сначала функция проверяет ввод непустого значения (еще один пример, когда важно использовать двойные кавычки, чтобы предотвратить появление сообщения об ошибке в случае ввода пустой строки). Затем, в строке , она проверяет наличие знака «минус» и в строке удаляет из введенного значения все цифры. Если в результате получилась непустая строка, значит, введено значение, не являющееся целым числом, и функция возвращает признак ошибки.
Если введенное значение допустимо, оно сравнивается с минимальным и максимальным значениями . Наконец, в случае ошибки функция возвращает 1 и 0 — в случае успеха.
Запуск сценария
Весь сценарий целиком является функцией. Его можно скопировать в другой сценарий или подключить как библиотечный файл. Чтобы преобразовать его в команду, просто добавьте в конец файла код из листинга 1.10.
Листинг 1.10. Дополнительная поддержка, превращающая сценарий в самостоятельную команду
# Проверка ввода
if validint "$1" "$2" "$3"; then
····echo "Input is a valid integer within your constraints."
Результаты
После добавления кода из листинга 1.10, сценарий можно использовать, как показано в листинге 1.11:
Листинг 1.11. Тестирование сценария validint
$ validint 1234.3
Invalid number format! Only digits, no commas, spaces, etc.
$ validint 103 1 100
Your value is too big: largest acceptable value is 100.
$ validint -17 0 25
Your value is too small: smallest acceptable value is 0.
$ validint -17 -20 25
Input is a valid integer within your constraints.
Усовершенствование сценария
Обратите внимание на строку , которая проверяет, не является ли первый символ знаком «минус»:
if ["${number%${number#?}}" = "-"]; then
Если первый символ действительно является знаком «минус», переменной testvalue присваивается числовая часть значения. Затем из этого неотрицательного значения удаляются все цифры и выполняется следующая проверка.
В данном случае велик соблазн использовать логический оператор И (-a), чтобы объединить выражения и избавиться от вложенных инструкций if. Например, на первый взгляд кажется, что следующий код должен работать:
if [! -z $min −a "$number" −lt "$min"]; then
··echo "Your value is too small: smallest acceptable value is $min." >&2
Но он не работает, потому что, даже если первое выражение, слева от оператора И, вернет ложное значение, нет никаких гарантий, что вторая проверка не будет выполнена (хотя в большинстве других языков программирования получилось бы именно так). То есть вы рискуете столкнуться со множеством ошибок из-за сравнения недействительных или неожиданных значений. Так быть не должно, но таковы реалии программирования на языке командной оболочки.
№ 6. Проверка ввода: вещественные числа
Проверка вещественных значений (с плавающей точкой) при ограниченных возможностях командной оболочки на первый взгляд кажется сложнейшей задачей, но представьте, что вещественное число состоит из двух целых чисел, разделенных десятичной точкой. Добавьте сюда возможность сослаться на другой сценарий (validint), и вы удивитесь, насколько короткой бывает проверка вещественных значений. Сценарий в листинге 1.12 предполагает, что находится в одном каталоге со сценарием validint.
Листинг 1.12. Сценарий validfloat
#!/bin/bash
# validfloat — Проверяет допустимость вещественного значения.
#·· Имейте в виду, что сценарий не распознает научную форму записи (1.304e5).
# Чтобы проверить вещественное значение, его нужно разбить на две части:
#·· целую и дробную. Первая часть проверяется как обычное целое число,
#·· а дробная — как положительное целое число. То есть число -30.5 оценивается
#·· как допустимое, а -30.-8 нет.
# Подключение других сценариев к текущему осуществляется с помощью оператора"."
# Довольно просто.
. validint
validfloat()
··fvalue="$1"
··# Проверить наличие десятичной точки.
··if [! -z $(echo $fvalue | sed 's/[^.]//g')]; then
····# Извлечь целую часть числа, слева от десятичной точки.
····decimalPart="$(echo $fvalue | cut −d. -f1)"
····# Извлечь дробную часть числа, справа от десятичной точки.
····fractionalPart="${fvalue#*.}"
····# Проверить целую часть числа, слева от десятичной точки
····if [! -z $decimalPart]; then
······# "!" инвертирует логику проверки, то есть ниже проверяется
······#·· "если НЕ допустимое целое число"
······if! validint "$decimalPart" "" ""; then
········return 1
····# Теперь проверим дробную часть.
····# Прежде всего, она не может содержать знак "минус" после десятичной точки,
····#·· например: 33.-11, поэтому проверим знак '-’ в дробной части.
····if ["${fractionalPart%${fractionalPart#?}}" = "-"]; then
······echo "Invalid floating-point number: '-' not allowed
········after decimal point." >&2
······return 1
····if ["$fractionalPart"!= ""]; then
······# Если дробная часть НЕ является допустимым целым числом…
······if! validint "$fractionalPart" "0" ""; then
········return 1
····# Если все значение состоит из единственного знака "-",
····#·· это недопустимое значение.
····if ["$fvalue" = "-"]; then
······echo "Invalid floating-point format." >&2
······return 1
····# В заключение проверить, что оставшиеся цифры представляют
····# допустимое целое число.
····if! validint "$fvalue" "" ""; then
······return 1
··return 0
Как это работает
Сценарий сначала проверяет наличие десятичной точки во входном значении . Если точки в числе нет, это не вещественное число. Далее для анализа извлекаются целая и дробная части числа. Затем, в строке , сценарий проверяет, является ли целая часть (слева от десятичной точки) допустимым целым числом. Следующая последовательность проверок сложнее, потому что требуется проверить отсутствие дополнительного знака «минус» (чтобы исключить такие странные числа, как 17. –30) и убедиться, что дробная часть (справа от десятичной точки) является допустимым целым числом.
Последняя проверка в строке выясняет, не является ли проверяемое значение единственным знаком «минус» (такое число выглядело бы слишком странно, чтобы пропустить его).
Все проверки выполнились успешно? Тогда сценарий возвращает 0, указывающий, что ввод пользователя содержит допустимое вещественное число.
Запуск сценария
Если во время выполнения функции не будет выведено сообщения об ошибке, она вернет 0 для числа, являющегося допустимым вещественным значением. Чтобы протестировать сценарий, добавьте в конец следующие строки кода:
if validfloat $1; then
··echo "$1 is a valid floating-point value."
Если попытка подключить сценарий validint сгенерирует ошибку, убедитесь, что он находится в одном из каталогов, перечисленных в PATH, или просто скопируйте функцию validint непосредственно в начало сценария validfloat.
Результаты
Сценарий validfloat принимает единственный аргумент для проверки. Листинг 1.13 демонстрирует проверку нескольких значений с помощью validfloat.
Листинг 1.13. Тестирование сценария validfloat
$ validfloat 1234.56
1234.56 is a valid floating-point value.
$ validfloat -1234.56
-1234.56 is a valid floating-point value.
$ validfloat —.75
-.75 is a valid floating-point value.
$ validfloat -11.-12
Invalid floating-point number: '-' not allowed after decimal point.
$ validfloat 1.0344e22
Invalid number format! Only digits, no commas, spaces, etc.
Если вы увидите лишний вывод, это может объясняться присутствием строк, добавленных ранее в validint для тестирования, которые вы забыли удалить перед переходом к этому сценарию. Просто вернитесь назад, к описанию сценария № 5 и закомментируйте или удалите строки, добавленные для тестирования функции.
Усовершенствование сценария
Было бы круто добавить в функцию поддержку научной формы записи, продемонстрированной в последнем примере. Это не так уж трудно. Вам нужно проверить присутствие в числе символа 'e’ или 'E’ и затем разбить его на три сегмента: целую часть (всегда представлена единственной цифрой), дробную часть и степень числа 10. После этого каждую часть можно проверить с помощью validint.
№ 7. Проверка форматов дат
Одна из наиболее сложных, но очень важная команда проверки — это проверка допустимости дат. Если не принимать в расчет високосные годы, задача не кажется особенно трудной, потому что каждый год календарь остается неизменным. В данном случае достаточно иметь таблицу с числом дней в месяцах и использовать ее для проверки каждой конкретной даты. Чтобы учесть високосные годы, нужно добавить в сценарий дополнительную логику, и именно этот аспект вызывает наибольшие сложности.
Ниже приводится набор критериев, проверка которых позволяет сказать, является ли проверяемый год високосным:
• Если год не кратен 4, он не високосный.
• Если год делится на 4 и на 400 — это високосный год.
• Если год делится на 4 и не делится на 400, но делится на 100 — это не високосный год.
• Все остальные годы, кратные 4, являются високосными.
Просматривая исходный код в листинге 1.14, обратите внимание, что для нормализации исходной даты перед проверкой этот сценарий использует normdate.
Листинг 1.14. Сценарий valid-date
··#!/bin/bash
··# valid-date — Проверяет дату с учетом правил определения високосных лет
··normdate="укажите здесь имя файла, в котором вы сохранили сценарий normdate.sh"
··exceedsDaysInMonth()
····# С учетом названия месяца и числа дней в этом месяце, данная функция
····# вернет: 0, если указанное число меньше или равно числу дней в месяце;
····# 1 — в противном случае.
····case $(echo $1|tr '[: upper: ]' '[: lower: ]') in
······jan*) days=31;; feb*) days=28;;
······mar*) days=31;; apr*) days=30;;
······may*) days=31;; jun*) days=30;;
······jul*) days=31;; aug*) days=31;;
······sep*) days=30;; oct*) days=31;;
······nov*) days=30;; dec*) days=31;;
········*) echo "$0: Unknown month name $1" >&2
············exit 1
····if [$2 −lt 1 −o $2 −gt $days]; then
······return 1
······return 0 # Число месяца допустимо.
··isLeapYear()
····# Эта функция возвращает 0, если указанный год является високосным;
····#·· иначе возвращается 1.
····# Правила проверки високосного года:
····#·· 1. Если год не делится на 4, значит, он не високосный.
····#·· 2. Если год делится на 4 и на 400, значит, он високосный.
····#·· 3. Если год делится на 4, не делится на 400 и делится
····#······на 100, значит, он не високосный.
····#·· 4. Любой другой год, который делится на 4, является високосным.
····year=$1
····if ["$((year % 4))" −ne 0]; then
······return 1 # Nope, not a leap year.
····elif ["$((year % 400))" −eq 0]; then
······return 0 # Yes, it's a leap year.
····elif ["$((year % 100))" −eq 0]; then
······return 1
······return 0
··# Начало основного сценария
··# =================
··if [$# −ne 3]; then
····echo "Usage: $0 month day year" >&2
····echo "Typical input formats are August 3 1962 and 8 3 1962" >&2
····exit 1
··# Нормализовать дату и сохранить для проверки на ошибки.
··newdate="$($normdate "$@")"
··if [$? -eq 1]; then
····exit 1 # Error condition already reported by normdate
··# Разбить нормализованную дату, в которой
··#·· первое слово = месяц, второе слово = число месяца
··#·· третье слово = год.
··month="$(echo $newdate | cut −d −f1)"
··day="$(echo $newdate | cut −d −f2)"
··year="$(echo $newdate | cut −d −f3)"
··# После нормализации данных проверить допустимость
··#·· числа месяца (например, Jan 36 является недопустимой датой).
··if! exceedsDaysInMonth $month "$2"; then
····if ["$month" = "Feb" −a "$2" −eq "29"]; then
······if! isLeapYear $3; then
········echo "$0: $3 is not a leap year, so Feb doesn't have 29 days." >&2
········exit 1
······echo "$0: bad day value: $month doesn't have $2 days." >&2
······exit 1
··echo "Valid date: $newdate"
Как это работает
Этот сценарий было очень интересно писать, потому что он требует проверки большого количества непростых условий: числа месяца, високосного года и так далее. Логика сценария не просто проверяет месяц как число от 1 до 12 или день — от 1 до 31. Чтобы сценарий проще было писать и читать, в нем используются специализированные функции.
Первая функция, exceedsDaysInMonth(), анализирует месяц, указанный пользователем, разрешая вероятные допущения (например, пользователь может передать название JANUAR, и оно будет правильно опознано). Анализ выполняется инструкцией case в строке , которая преобразует свой аргумент в нижний регистр и затем сравнивает полученное значение с константами, чтобы получить число дней в месяце. Единственный недостаток — для февраля функция всегда возвращает 28 дней.
Вторая функция, isLeapYear(), с помощью простых арифметических проверок выясняет, содержит ли февраль в указанном году 29-е число .
В основном сценарии исходные данные передаются сценарию normdate, представленному выше, для нормализации и затем разбиваются на три поля: $month, $day и $year. Затем вызывается функция exceedsDaysInMonth для проверки допустимости указанного числа для данного месяца, при этом 29 февраля обрабатывается отдельно — в этом случае вызовом функции isLeapYear проверяется год и при необходимости выводится сообщение об ошибке. Если пользовательская дата успешно преодолела все проверки, значит, она допустимая!
Запуск сценария
Запуская сценарий (как показано в листинге 1.15), введите в командной строке дату в формате месяц-день-год. Месяц можно указать в виде трехсимвольного сокращения, полного названия или числа; год должен состоять из четырех цифр.
Результаты
Листинг 1.15. Тестирование сценария valid-date
$ valid-date august 3 1960
Valid date: Aug 3 1960
$ valid-date 9 31 2001
valid-date: bad day value: Sep doesn’t have 31 days.
$ valid-date feb 29 2004
Valid date: Feb 29 2004
$ valid-date feb 29 2014
valid-date: 2014 is not a leap year, so Feb doesn’t have 29 days.
Усовершенствование сценария
Подход, аналогичный используемому в этом сценарии, можно применить для проверки значения времени в 24-часовом формате или в 12-часовом формате с суффиксом AM/PM (Ante Meridiem/Post Meridiem — пополуночи/пополудни). Разбив значение времени по двоеточиям, нужно убедиться, что число минут и секунд (если указано) находится в диапазоне от 0 до 59, и затем проверить первое поле на вхождение в диапазон от 0 до 12, если присутствует суффикс AM/PM, или от 0 до 24, если предполагается 24-часовой формат. К счастью, несмотря на существование секунд координации (високосных секунд) и других небольших корректировок, помогающих сохранить сбалансированность календарного времени, их можно игнорировать в повседневной работе, то есть нет необходимости использовать замысловатые вычисления.
При наличии доступа к GNU-команде date в Unix или GNU/Linux можно использовать совершенно иной способ проверки високосных лет. Попробуйте выполнить следующую команду и посмотрите, что получится:
$ date −d 12/31/1996 +%j
Если у вас в системе используется новейшая, улучшенная версия date, вы получите результат 366. Более старая версия просто пожалуется на ошибочный формат входных данных. Теперь подумайте о результате, возвращаемом новейшей командой date. Сможете ли вы написать двухстрочную функцию, проверяющую високосный год?
Наконец, данный сценарий слишком терпимо относится к названиям месяцев, например, название febmama будет опознано как допустимое, потому что инструкция case в строке проверяет только первые три буквы. Эту проблему можно устранить, организовав точную проверку общепринятых сокращений (таких как feb) и полных названий месяцев (february), и даже некоторых типичных опечаток (febuary). Все это легко реализуется, было бы желание!
№ 8. Улучшение некачественных реализаций echo
Как упоминалось в разделе «Что такое POSIX?» в начале этой главе, большинство современных реализаций Unix и GNU/Linux включают команду echo, поддерживающую флаг −n, который подавляет вывод символа перевода строки в конце, но такая поддержка имеется не во всех реализациях. Некоторые для подавления поведения по умолчанию используют специальный символ c, другие просто добавляют символ перевода строки, не давая никакой возможности изменить это поведение.
Выяснить, какая реализация echo используется в текущей системе, довольно просто: введите следующие команды и посмотрите, что из этого получится:
$ echo −n "The rain in Spain"; echo " falls mainly on the Plain"
Если команда echo поддерживает флаг −n, вы увидите следующий вывод:
The rain in Spain falls mainly on the Plain
Если нет, вывод будет иметь следующий вид:
-n The rain in Spain
falls mainly on the Plain
Гарантировать определенный формат вывода очень важно, и эта важность будет расти с увеличением интерактивности сценариев. Так что мы напишем альтернативную версию echo, с именем echon, которая всегда будет подавлять вывод завершающего символа перевода строки. Благодаря этому мы получим достаточно надежный инструмент, который сможем использовать, когда понадобится функциональность echo −n.
Способов исправить проблему с командой echo так же много, как страниц в этой книге. Но больше всего нам нравится очень компактная реализация, которая просто фильтрует ввод с помощью команды awk printf, как показано в листинге 1.16.
Листинг 1.16. Простая альтернатива echo, использующая команду awk printf
··echo "$*" | awk '{ printf "%s", $0 }'
Однако есть возможность избежать накладных расходов на вызов команды awk. Если у вас в системе имеется команда printf, используйте ее в сценарии echon, как показано в листинге 1.17.
Листинг 1.17. Альтернатива echo, использующая команду printf
··printf "%s" "$*"
А как быть, если команды printf нет и вы не желаете использовать awk? Тогда отсекайте любые завершающие символы перевода строки с помощью команды tr, как показано в листинге 1.18.
Листинг 1.18. Простая альтернатива echo, использующая команду tr
··echo "$*" | tr −d 'n'
Это простой и эффективный способ с хорошей переносимостью.
Запуск сценария
Просто добавьте этот сценарий в каталог из списка PATH, и вы сможете заменить все вызовы echo −n командой echon, надежно помещающей текстовый курсор в конец строки после вывода.
Результаты
Для демонстрации функции echon сценарий принимает аргумент и выводит его, затем читает ввод пользователя. В листинге 1.19 показан сеанс тестирования сценария.
Листинг 1.19. Тестирование команды echon
$ echon "Enter coordinates for satellite acquisition: "
Enter coordinates for satellite acquisition: 12,34
Усовершенствование сценария
Скажем честно: тот факт, что одни командные оболочки имеют команду echo, поддерживающую флаг −n, другие предполагают использование специального символа c в конце вывода, а третьи вообще не дают возможности подавить отображение символа перевода строки, доставляет массу проблем создателям сценариев. Чтобы устранить это несоответствие, можно написать свою функцию, которая автоматически проверит поведение echo, определит, какая версия используется в системе и затем изменит вызов соответственно. Например, можно выполнить команду echo −n hi | wc −c и проверить количество символов в результате: два (hi), три (hi плюс символ перевода строки), четыре (-n hi) или пять (-n hi плюс символ перевода строки).
№ 9. Вычисления произвольной точности с вещественными числами
В сценариях часто используется синтаксическая конструкция $(()), позволяющая выполнять вычисления с использованием простейших математических функций. Эта конструкция может очень пригодиться для упрощения таких распространенных операций, как увеличение на единицу переменных-счетчиков. Она поддерживает операции сложения, вычитания, деления, деления по модулю (остаток от деления нацело) и умножения, но только с целыми числами. Другими словами, следующая команда вернет 0, а не 0,5:
echo $((1 / 2))
То есть вычисления с большей точностью превращаются в проблему. Существует не так много хороших программ-калькуляторов, работающих в командной строке. Одна из них — замечательная программа bc, которой владеют очень немногие пользователи Unix. Позиционирующая себя как калькулятор для вычислений с произвольной точностью, bc появилась на заре развития Unix, славится малопонятными сообщениями об ошибках и отсутствием подсказок. Предполагается, что пользователь и так знает, что делает. Но в этом есть свои плюсы. Мы можем написать сценарий-обертку, делающий программу bc более дружественной, как показано в листинге 1.20.
Листинг 1.20. Сценарий scriptbc
··#!/bin/bash
··# scriptbc — обертка для 'bc’, возвращающая результат вычислений
··if ["$1" = "-p"]; then
····precision=$2
····shift 2
····precision=2 # По умолчанию
··bc −q — l << EOF
····scale=$precision
Как это работает
Синтаксис << в строке позволяет включить в сценарий произвольное содержимое и интерпретировать его как текст, введенный непосредственно в поток ввода, что в данном случае дает простой способ передачи команд программе bc. Такие вставки называют встроенными документами (here document). Вслед за парой символов << помещается текстовая метка, которая будет интерпретироваться как признак конца такого потока ввода (при условии, что она находится в отдельной строке). В листинге 1.20 используется метка EOF.
Этот сценарий демонстрирует также, как использовать аргументы для увеличения гибкости команд. В данном случае сценарий можно вызвать с флагом −p и указать желаемую точность чисел для вывода. Если точность не указана, по умолчанию используется точность scale=2 .
Работая с программой bc, важно понимать разницу между ее параметрами length (длина) и scale (точность). В терминологии bc под длиной (length) понимается общее количество цифр в числе, а под точностью (scale) — количество цифр после десятичной точки. То есть число 10,25 имеет длину 4 и точность 2, а число 3,14159 имеет длину 6 и точность 5.
По умолчанию bc имеет переменное значение для length, но, так как параметр scale по умолчанию получает нулевое значение, без параметров программа bc действует подобно синтаксической конструкции $(()). К счастью, если в вызов bc добавить параметр scale, она продемонстрирует огромную скрытую мощь, как показано в следующем примере, где вычисляется количество недель между 1962 и 2002 годами (исключая високосные дни):
bc 1.06.95
Copyright 1991–1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation,
This is free software with ABSOLUTELY NO WARRANTY.
For details type 'warranty’.
(2002–1962)*365
2085.7142857142
Чтобы получить доступ к возможностям bc из командной строки, сценарий-обертка должен удалить начальную информацию об авторских правах, если она имеется, однако большинство реализаций bc автоматически подавляют вывод начального баннера, если вводом является не терминал (stdin). Кроме того, сценарий-обертка определяет довольно разумное значение для масштаба (scale), передает программе bc фактическое выражение и затем завершает ее командой quit.
Запуск сценария
Чтобы запустить сценарий, передайте математическое выражение программе в виде аргумента, как показано в листинге 1.21.
Результаты
Листинг 1.21. Тестирование сценария scriptbc
$ scriptbc 14600/7
$ scriptbc −p 10 14600/7
2085.7142857142
№ 10. Блокировка файлов
Любому сценарию, читающему или записывающему данные в общий файл, например в файл журнала, необходим надежный способ блокировки файлов, чтобы другие экземпляры сценария не могли по ошибке затереть данные в файле до того, как он перестанет использоваться. Для этого часто создается отдельный файл-блокировка для каждого используемого файла. Наличие файла-блокировки играет роль семафора, или индикатора, сообщающего, что файл задействован другим сценарием и не должен использоваться. Запрашивающий сценарий в этом случае многократно проверяет наличие файла-блокировки, ожидая его удаления, после которого файл можно свободно использовать.
Однако применение файлов-блокировок сопряжено с большими трудностями, потому что многие решения, кажущиеся надежными, в действительности очень ненадежны. Например, для организации блокировки доступа к файлам часто используется следующее решение:
while [-f $lockfile]; do
touch $lockfile
Кажется, что такое решение должно работать. Или нет? Сценарий в цикле проверяет присутствие файла-блокировки и, как только он исчезает, тут же создает собственный, чтобы в безопасности изменить рабочий файл. Если в это время другой сценарий увидит файл-блокировку, то продолжит выполнять цикл ожидания, пока тот не исчезнет. Однако на практике такой способ не работает. Представьте, что сразу после выхода из цикла while, но перед вызовом команды touch диспетчер задач приостановит сценарий, дав возможность поработать другому сценарию.
Если вам непонятно о чем речь, вспомните, что хотя кажется, что компьютер делает что-то одно, в действительности он выполняет сразу несколько программ, переключаясь между ними через короткие интервалы времени. Проблема в том, что между завершением цикла, проверяющего существование файла-блокировки, и созданием нового проходит время, в течение которого система может переключиться с одного сценария на другой, а тот в свою очередь благополучно убедится в отсутствии файла-блокировки и создаст свою версию. Затем система переключится на первый сценарий, который тут же выполнит команду touch. В результате оба сценария будут считать, что имеют исключительный доступ к файлу-блокировке, то есть сложится ситуация, которой мы пытаемся избежать.
К счастью, Стефан ван ден Берг (Stephen van den Berg) и Филип Гюнтер (Philip Guenther), авторы программы procmail для фильтрации электронной почты, также создали утилиту командной строки lockfile, которая дает возможность безопасной и надежной работы с файлами-блокировками в сценариях командной оболочки.
Многие реализации Unix, включая GNU/Linux и OS X, устанавливают утилиту lockfile по умолчанию. Ее присутствие в системе можно проверить простой командой man 1 lockfile. Если в результате откроется страница справочного руководства, значит, удача сопутствует вам! Сценарий в листинге 1.22 предполагает наличие команды lockfile, и все последующие сценарии требуют работоспособности механизма надежной блокировки, реализованного в сценарии № 10, поэтому перед их использованием также проверьте наличие команды lockfile в вашей системе.
Листинг 1.22. Сценарий filelock
··#!/bin/bash
··# filelock — Гибкий механизм блокировки файлов
··retries="10"·········· # Число попыток по умолчанию
··action="lock"··········# Действие по умолчанию
··nullcmd="’which true’" # Пустая команда для lockfile
··while getopts "lur: " opt; do
····case $opt in
······l) action="lock";;
······u) action="unlock";;
······r) retries="$OPTARG";;
··shift $(($OPTIND — 1))
··if [$# −eq 0]; then # Вывести в stdout многострочное сообщение об ошибке.
····cat << EOF >&2
······Usage: $0 [-l|-u] [-r retries] LOCKFILE
······Where −l requests a lock (the default), −u requests an unlock, −r X
······specifies a max number of retries before it fails (default = $retries).
····exit 1
··# Проверка наличия команды lockfile.
··if [-z "$(which lockfile | grep −v '^no ')"]; then
····echo "$0 failed: 'lockfile' utility not found in PATH." >&2
····exit 1
··if ["$action" = "lock"]; then
····if! lockfile -1 −r $retries "$1" 2> /dev/null; then
······echo "$0: Failed: Couldn't create lockfile in time." >&2
······exit 1
··else # Действие = разблокировка
····if [! -f "$1"]; then
······echo "$0: Warning: lockfile $1 doesn't exist to unlock." >&2
······exit 1
····rm −f "$1"
Как это работает
Как это часто бывает с хорошо написанными сценариями командной оболочки, половину листинга 1.22 занимает анализ входных данных и проверка на наличие ошибок. Затем выполняется инструкция if и осуществляется фактическая попытка использовать системную команду lockfile. Она вызывается с заданным числом попыток и генерирует собственное сообщение об ошибке, если ей так и не удалось заблокировать файл. А что произойдет, если предложить сценарию снять блокировку (например, удалить файл-блокировку), которой в действительности нет? В результате будет сгенерировано другое сообщение об ошибке. В противном случае lockfile просто удалит блокировку.
Если говорить более конкретно, первый блок использует мощную функцию getopts для анализа всех поддерживаемых флагов (-l, −u, — r) в цикле while. Это наиболее типичный способ использования getopts, который снова и снова будет встречаться в книге. Обратите внимание на команду shift $(($OPTIND — 1)) в строке : переменная OPTIND устанавливается функцией getopts, благодаря чему сценарий получает возможность сдвинуть входные параметры вниз (то есть значение параметра $2 сместится в параметр $1, например), вытолкнув тем самым обработанные параметры, начинающиеся с дефиса.
Поскольку этот сценарий использует системную утилиту lockfile, он сначала проверяет ее доступность в списке путей пользователя и завершается с сообщением об ошибке, если утилита недоступна. Далее следует простая условная инструкция , выясняющая, какая операция запрошена — блокировка или разблокировка, — и производится соответствующий вызов утилиты lockfile.
Запуск сценария
Сценарий filelock относится к категории сценариев, которые редко используются сами по себе, и для его проверки потребуется открыть два окна терминала. Чтобы установить блокировку, просто укажите имя файла, который будет играть роль блокировки, в аргументе сценария filelock. Чтобы снять блокировку, запустите сценарий еще раз с флагом −u.
Результаты
Сначала создадим заблокированный файл, как показано в листинге 1.23.
Листинг 1.23. Создание файла-блокировки командой filelock
$ filelock /tmp/exclusive.lck
$ ls −l /tmp/exclusive.lck
-r-r-r-··1 taylor··wheel··1 Mar 21 15:35 /tmp/exclusive.lck
Когда в следующий раз вы попытаетесь установить ту же блокировку, filelock выполнит указанное количество попыток (10 по умолчанию) и завершится с ошибкой (как показано в листинге 1.24):
Листинг 1.24. Ошибка при попытке создать файл-блокировку обращением к сценарию filelock
$ filelock /tmp/exclusive.lck
filelock: Failed: Couldn’t create lockfile in time.
Завершив работу с файлом, можно освободить блокировку, как показано в листинге 1.25.
Листинг 1.25. Освобождение блокировки с помощью сценария filelock
$ filelock −u /tmp/exclusive.lck
Чтобы увидеть, как сценарий действует в двух терминалах, выполните команду разблокировки в одном из них, пока в другом сценарий крутится в цикле, пытаясь приобрести блокировку.
Усовершенствование сценария
Поскольку наличие блокировки определяется сценарием, было бы полезно добавить еще один параметр, ограничивающий время ее действия. Если команда lockfile завершится неудачей, можно проверить последнее время доступа к файлу-блокировке и, если он старше значения этого параметра, безопасно удалить его, добавив, при желании, вывод предупреждающего сообщения.
Скорее всего, это не затронет вас, но lockfile не поддерживает работу с сетевой файловой системой (NFS) на смонтированных сетевых устройствах. Действительно надежный механизм блокировки файлов в NFS чрезвычайно сложен в реализации. Лучшее решение этой проблемы — всегда создавать файлы-блокировки только на локальных дисках или задействовать специализированный сценарий, способный управлять блокировками, используемыми несколькими системами.
№ 11. ANSI-последовательности управления цветом
Вероятно, вы замечали, что разные приложения командной строки поддерживают разные стили отображения текста. Существует большое количество вариантов оформления. Например, сценарий может выводить определенные слова жирным шрифтом или красным цветом на желтом фоне. Однако работать с ANSI-последовательностями (American National Standards Institute — американский национальный институт стандартов) очень неудобно из-за их сложности. Чтобы упростить их применение, в листинге 1.26 создается набор переменных, значениями которых являются ANSI-последовательности, управляющие цветом и форматированием.
Листинг 1.26. Функция initializeANSI
#!/bin/bash
# ANSI-последовательности управления цветом — используйте эти переменные
#·· для управления цветом и форматом выводимого текста.
#·· Имена переменных, оканчивающиеся символом 'f’, соответствуют цветам шрифта
#·· (foreground), а имена переменных, оканчивающиеся символом 'b’, соответствуют
#·· цветам фона (background).
initializeANSI()
··esc=" 33" # Если эта последовательность не будет работать,
············ #·· введите символ ESC непосредственно.
··# Цвета шрифта
··blackf="${esc}[30m";··redf="${esc}[31m";·· greenf="${esc}[32m"
··yellowf="${esc}[33m"··bluef="${esc}[34m";··purplef="${esc}[35m"
··cyanf="${esc}[36m";·· whitef="${esc}[37m"
··# Цвета фона
··blackb="${esc}[40m";··redb="${esc}[41m";·· greenb="${esc}[42m"
··yellowb="${esc}[43m"··blueb="${esc}[44m";··purpleb="${esc}[45m"
··cyanb="${esc}[46m";·· whiteb="${esc}[47m"
··# Жирный, наклонный, с подчеркиванием и инверсное отображение
··boldon="${esc}[1m";···· boldoff="${esc}[22m"
··italicson="${esc}[3m";··italicsoff="${esc}[23m"
··ulon="${esc}[4m";······ uloff="${esc}[24m"
··invon="${esc}[7m";······invoff="${esc}[27m"
··reset="${esc}[0m"
Как это работает
Если вы привыкли использовать язык разметки HTML, работа с этими последовательностями может показаться вам слишком сложной. В HTML вы просто вставляете открывающие теги и закрываете их в обратном порядке, следя за тем, чтобы закрыть все открытые теги. Чтобы выделить наклонным шрифтом фрагмент приложения, отображаемого жирным шрифтом, можно написать такой код HTML:
<b>this is in bold and <i>this is italics</i> within the bold</b>
Попытка закрыть тег, управляющий жирностью шрифта, раньше, чем тег, управляющий наклонным отображением, может вызвать беспорядок в отдельных веб-браузерах. Но в случае с ANSI-последовательностями дело обстоит иначе: некоторые из них фактически отменяют действие предыдущих, а также существует общая последовательность сброса, отменяющая действие всех других. Ее обязательно нужно добавить в конце вывода, а за последовательностью, включающей тот или иной режим форматирования, должна идти соответствующая ей последовательность, выключающая этот режим. Использовав переменные из сценария, предыдущее предложение можно вывести, как показано ниже:
${boldon}this is in bold and ${italicson}this is
italics${italicsoff}within the bold${reset}
Запуск сценария
Чтобы опробовать этот сценарий, нужно сначала вызвать функцию инициализации, а затем выполнить несколько команд echo с разными комбинациями цвета и эффектами форматирования:
initializeANSI
echo −e "${yellowf}This is a phrase in yellow${redb} and red${reset}"
echo −e "${boldon}This is bold${ulon} this is ul${reset} bye-bye"
echo −e "${italicson}This is italics${italicsoff} and this is not"
echo −e "${ulon}This is ul${uloff} and this is not"
echo −e "${invon}This is inv${invoff} and this is not"
echo −e "${yellowf}${redb}Warning I ${yellowb}${redf}Warning II${reset}"
Результаты
Результаты работы сценария в листинге 1.27, воспроизведенные в книге, не впечатляют, но на экране, где поддерживаются все управляющие последовательности, они определенно привлекут ваше внимание.
Листинг 1.27. Как можно оформить текст с применением переменных из листинга 1.26
This is a phrase in yellow and red
This is bold this is ul bye-bye
This is italics and this is not
This is ul and this is not
This is inv and this is not
Warning I Warning II
Усовершенствование сценария
Запустив этот сценарий, можно увидеть такой вывод: