Идиомы bash [Карл Олбинг] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

Beijing

Boston Farnham Sebastopol

Tokyo

Идиомы
bash
мощные, гибкие и понятные
сценарии командной оболочки

Карл Олбинг, Джей Пи Фоссен

2023

ББК 32.973.2-018.2
УДК 004.451.9
О-53

Олбинг Карл, Фоссен Джей Пи
О-53 Идиомы bash. — СПб.: Питер, 2023. — 208 с.: ил. — (Серия «Бестселлеры
O’Reilly»).
ISBN 978-5-4461-2307-0
Сценарии на языке командной оболочки получили самое широкое распространение, особенно написанные на языках, совместимых с bash. Но эти сценарии часто сложны и непонятны.
Сложность — враг безопасности и причина неудобочитаемости кода. Эта книга на практических
примерах покажет, как расшифровывать старые сценарии и писать новый код, максимально понятный и легко читаемый.
Авторы покажут, как использовать мощь и гибкость командной оболочки. Даже если вы умеете писать сценарии на bash, эта книга поможет расширить ваши знания и навыки. Независимо
от используемой ОС — Linux, Unix, Windows или Mac — к концу книги вы научитесь понимать
и писать сценарии на экспертном уровне. Это вам обязательно пригодится.
Вы познакомитесь с идиомами, которые следует использовать, и такими, которых следует
избегать.

16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)

ББК 32.973.2-018.2
УДК 004.451.9
Права на издание получены по соглашению с O’Reilly.
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было
форме без письменного разрешения владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как
надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не
может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за
возможные ошибки, связанные с использованием книги.
Издательство не несет ответственности за доступность материалов, ссылки на которые вы можете найти
в этой книге. На момент подготовки книги к изданию все ссылки на интернет-ресурсы были действующими.

ISBN 978-1492094739 англ.

ISBN 978-5-4461-2307-0

 uthorized Russian translation of the English edition of bash Idioms, ISBN
A
9781492094753 © 2022 Carl Albing and JP Vossen.
This translation is published and sold by permission of O’Reilly Media, Inc.,
which owns or controls all rights to publish and sell the same.
© Перевод на русский язык ООО «Прогресс книга», 2022
© Издание на русском языке, оформление ООО «Прогресс книга», 2023
© Серия «Бестселлеры O’Reilly», 2023

Оглавление

Вступление................................................................................................................10
Запуск bash.......................................................................................................................................12
Управление версиями.................................................................................................................13
Hello World........................................................................................................................................14
Условные обозначения...............................................................................................................14
Использование исходного кода примеров.......................................................................15
Благодарности.........................................................................................................17
От издательства.......................................................................................................19
Глава 1. Идиома «большого» if..............................................................................20
«Большой» if.....................................................................................................................................20
Или ELSE.............................................................................................................................................22
Выполняем несколько команд................................................................................................23
Еще о случае нескольких команд..........................................................................................25
Так делать не нужно!....................................................................................................................25
В заключение: стиль и удобочитаемость............................................................................27
Глава 2. Язык циклов...............................................................................................29
Циклические конструкции........................................................................................................29
Явные значения..............................................................................................................................31
Почти как в Python........................................................................................................................34
Кавычки и пробелы......................................................................................................................35
Разработка и тестирование циклов for...............................................................................37
Циклы while и until........................................................................................................................39
В заключение: стиль и удобочитаемость............................................................................39

6   Оглавление

Глава 3. На всякий случай: оператор Case...........................................................42
Сделайте свой выбор...................................................................................................................42
Применение на практике..........................................................................................................44
Задача..........................................................................................................................................45
Наш сценарий..........................................................................................................................45
Сценарии-обертки........................................................................................................................47
Еще один важный момент.........................................................................................................54
В заключение: стиль и удобочитаемость............................................................................55
Глава 4. Язык переменных.....................................................................................57
Ссылка на переменную...............................................................................................................57
Дополнительные параметры...................................................................................................59
Сокращенный вариант команды basename...............................................................59
Удаление пути или префикса............................................................................................60
Сокращенный вариант команды dirname или удаление суффикса................61
Другие модификаторы.........................................................................................................62
Условные подстановки...............................................................................................................66
Значения по умолчанию......................................................................................................66
Списки значений, разделенных запятыми..................................................................67
Изменение значения.............................................................................................................68
$RANDOM..........................................................................................................................................68
Подстановка команд....................................................................................................................69
В заключение: стиль и удобочитаемость............................................................................71
Глава 5. Выражения и арифметика.......................................................................72
Арифметика.....................................................................................................................................73
Круглые скобки не нужны...................................................................................................76
Составные команды.....................................................................................................................77
В заключение: cтиль и удобочитаемость............................................................................80
Глава 6. Функции......................................................................................................82
Вызов функций...............................................................................................................................82
Определение функций................................................................................................................83
Параметры функций..............................................................................................................83
Возвращаемые значения функций.................................................................................85
Локальные переменные......................................................................................................86

Оглавление  

7

Особые случаи................................................................................................................................87
Функция printf.................................................................................................................................88
Вывод POSIX..............................................................................................................................89
Получение и использование даты и времени...........................................................90
printf для повторного использования или отладки................................................91
В заключение: стиль и удобочитаемость............................................................................91
Глава 7. Списки и хеши............................................................................................93
Сходные черты...............................................................................................................................95
Списки................................................................................................................................................96
Хеши................................................................................................................................................. 101
Пример подсчета слов............................................................................................................. 106
В заключение: cтиль и удобочитаемость......................................................................... 109
Глава 8. Аргументы............................................................................................... 110
Ваш первый аргумент............................................................................................................... 110
Поддержка ключей.................................................................................................................... 112
Анализ ключей...................................................................................................................... 113
Длинные ключи........................................................................................................................... 115
HELP!................................................................................................................................................. 118
Отладочный и подробный режимы вывода.................................................................. 122
Версия.............................................................................................................................................. 123
В заключение: стиль и удобочитаемость......................................................................... 124
Глава 9. Файлы и не только................................................................................. 125
Чтение файлов............................................................................................................................. 125
read............................................................................................................................................. 125
mapfile....................................................................................................................................... 126
Метод «грубой силы»......................................................................................................... 130
Изменяем $IFS при чтении файлов.................................................................................... 130
Имитации файлов....................................................................................................................... 133
Настроечные каталоги............................................................................................................. 134
Организация библиотек.......................................................................................................... 135
Shebang!.......................................................................................................................................... 136
Строгий режим bash.................................................................................................................. 138
Код выхода..................................................................................................................................... 139

8   Оглавление

Это ловушка!................................................................................................................................. 140
Встроенные документы и строки........................................................................................ 142
Код выполняется в интерактивном режиме?................................................................ 143
В заключение................................................................................................................................ 144
Глава 10. Помимо идиом: работа с bash............................................................ 145
Приглашения к вводу............................................................................................................... 146
Часовой пояс в приглашении........................................................................................ 149
Получение ввода пользователя.......................................................................................... 149
read............................................................................................................................................. 150
pause.......................................................................................................................................... 152
select.......................................................................................................................................... 152
Псевдонимы.................................................................................................................................. 153
Функции.......................................................................................................................................... 155
Локальные переменные.......................................................................................................... 156
Возможности Readline.............................................................................................................. 156
Журналирование в bash.......................................................................................................... 158
Обработка JSON с помощью jq............................................................................................. 159
Поиск в списке процессов...................................................................................................... 160
Ротация старых файлов........................................................................................................... 161
Встроенная документация..................................................................................................... 163
Отладка в bash............................................................................................................................. 169
Модульное тестирование в bash......................................................................................... 172
В заключение................................................................................................................................ 173
Глава 11. Разработка своего руководства по стилю....................................... 174
Удобочитаемость........................................................................................................................ 177
Комментарии................................................................................................................................ 179
Имена............................................................................................................................................... 180
Функции.......................................................................................................................................... 182
Кавычки........................................................................................................................................... 183
Форматирование........................................................................................................................ 184
Синтаксис....................................................................................................................................... 186
Другие рекомендации.............................................................................................................. 187
Шаблон сценария....................................................................................................................... 187
Другие руководства по стилю.............................................................................................. 189

Оглавление  

9

Инструмент проверки оформления кода на bash....................................................... 190
В заключение................................................................................................................................ 191
Приложение. Руководство по стилю................................................................. 192
Удобочитаемость........................................................................................................................ 193
Комментарии................................................................................................................................ 194
Имена............................................................................................................................................... 194
Функции.......................................................................................................................................... 195
Кавычки........................................................................................................................................... 197
Форматирование........................................................................................................................ 198
Синтаксис....................................................................................................................................... 198
Другие рекомендации.............................................................................................................. 199
Шаблон сценария....................................................................................................................... 200
Об авторах............................................................................................................. 202
Иллюстрация на обложке.................................................................................... 203

Вступление

Вот как словарь Уэбстера определяет термин идиома:1
1. Специфический оборот речи, употребляющийся как единое
целое, значение которого не определяется значением входящих
в него слов (как, например, оборот «в подвешенном состоянии»,
означающий «неопределенность»). В данном обороте может иметь
место нетипичное грамматическое использование слов (например,
«дать дорогу»).
2а. Язык, свойственный народу, географической области, сообществу или классу, диалект.
2б. Синтаксическая, грамматическая или структурная форма,
характерная для языка.
3. Стиль или форма художественного выражения, характерные
для человека, периода или движения, средства или инструмента.
Почему для книги выбрано название «Идиомы bash»? Для простоты.
Или, если хотите, чтобы было понятнее. В этой книге «простота» —
синоним «понятности». Мы не собираемся убеждать вас в важности
удобочитаемости: если это не первая книга по программированию,
которую вы читаете, значит, уже должны это понимать. Удобочитаемость означает простоту чтения и понимания кода, особенно если
он написан не вами. Не менее важно научиться писать код так, чтобы
в будущем вы или кто-то другой смогли его понять. Очевидно, что
эти аспекты являются разными сторонами одной медали, поэтому
1

https://oreil.ly/pgx8b.

Вступление  11

мы рассмотрим и идиомы, которые следует использовать, и такие,
которых следует избегать.
Между нами говоря, мы считаем bash языком «управления». Для сложной обработки данных он малопригоден: ее можно реализовать, но код
получится слишком сложным. Однако, если все инструменты, необходимые для обработки данных, уже имеются и требуется лишь «склеить»
их, то bash подойдет на эту роль как нельзя лучше.
Если мы собираемся использовать bash только для управления, то
зачем беспокоиться об идиомах или «структурной форме» языка?
Программы развиваются, возможности ширятся, ситуация меняется,
но нет ничего более постоянного, чем временное. Рано или поздно
кому-то придется прочитать ваш код, понять его и изменить. Если он
написан с использованием непонятных идиом, то сделать это будет
намного сложнее.
Во многих отношениях bash не похож на другие языки. У него богатая
история (некоторые могут сказать «багаж»), и есть причины, почему он
выглядит и работает определенным образом. Мы не будем много говорить об этом. Если вам интересна эта тема, обратитесь к нашей книге
«bash Cookbook»1. Сценарии командной оболочки «управляют миром»,
по крайней мере в мирах Unix и Linux (а Linux фактически управляет
облачным миром), причем подавляющее большинство этих сценариев
написаны на bash. Поддержание обратной совместимости с самыми
первыми командными оболочками Unix часто критически важно, но
накладывает некоторые... ограничения.
Теперь о «диалектах». Наиболее важным, особенно для обратной совместимости, является стандарт POSIX. Об этом тоже не будем много
говорить, в конце концов эта книга посвящена идиомам bash, а не POSIX.
Появление других диалектов возможно, когда программисты пишут код
на bash в стиле другого известного им языка. Однако поток, имеющий
смысл в C, может показаться бессвязным в bash.
1

https://learning.oreilly.com/library/view/bash-cookbook-2nd/9781491975329/.

12  Вступление

Итак, в этой книге мы намерены продемонстрировать «стиль или форму... выражения, характерную» для bash (в духе третьего определения
в словаре Уэбстера). Программисты на Python называют свой стиль
pythonic. А мы бы хотели в этой книге показать стиль bashy.
К концу книги читатель приобретет следующие знания и навыки:
научится писать полезный, гибкий, удобочитаемый и... стильный
код на bash;
узнает,

как

расшифровываются

идиомы

bash,

такие

как

${MAKEMELC,,} и ${PATHNAME##*/};

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

Запуск bash
Мы предполагаем, что вы уже имеете опыт программирования на bash
и нет нужды рассказывать, где его найти или как установить. Конечно,
bash есть почти во всех дистрибутивах Linux, и он уже установлен по
умолчанию или может быть установлен практически в любой операционной системе. Получить версию для Windows можно с помощью Git
for Windows1 подсистемы Windows для Linux (Windows Subsystem for
Linux, WSL) или другими способами.

bash на Mac
Обратите внимание на версию bash, которая по умолчанию устанавливается в Mac: она довольно старая и не поддерживает многие новые
идиомы версий 4.0 и выше. Более свежую версию можно получить
1

https://gitforwindows.org/.

Управление версиями  

13

с помощью MacPorts, Homebrew или Fink. По информации от Apple1,
проблема в том, что новые версии bash используют GPLv3, что является
проблемой для macOS.
Apple также сообщает, что macOS Catalina и более новые версии будут
использовать Zsh в качестве интерактивной оболочки и оболочки входа
по умолчанию. Zsh в значительной мере совместима с bash, но некоторые примеры в этой книге потребуют изменений. Командная оболочка
bash на компьютерах Mac не исчезнет (по крайней мере, пока), причем
использование Zsh в качестве оболочки по умолчанию не повлияет на
строку «shebang» (см. раздел «Shebang!» в главе 9), но имейте в виду, что
если не обновить версию bash, вы застрянете в каменном веке.
Мы отметили примеры сценариев, несовместимые с Zsh, комментарием:
«Не работает в Zsh 5.4.2!».

bash в контейнерах
Будьте осторожны, используя Docker и другие создатели контейнеров,
где /bin/sh ссылается не на bash, а /bin/bash может вообще не существовать! Это особенно актуально для ограниченных окружений, включая
системы интернета вещей и промышленные контроллеры.
может ссылаться на bash (согласно POSIX), но также на Ash,
Dash, BusyBox (которым чаще всего является Dash) или что-то еще.
Будьте внимательны (см. раздел «Shebang!» в главе 9) и убедитесь, что
bash действительно установлен, или придерживайтесь стандарта POSIX
и избегайте «башизмов».
/bin/sh

Управление версиями
Очень надеемся, что вы используете какую-либо систему управления
версиями. Если да, то можете пропустить этот абзац. Если нет, то внедрите
1

https://oreil.ly/2PZRm.

14  Вступление

их в свою работу, прежде чем продолжать чтение. Мы посвятили этому
вопросу целое приложение в «bash Cookbook»1, но вообще в интернете
можно найти огромный объем информации о таких системах, в том числе
от одного из авторов этой книги2.

Hello World
Во многих трудах нужно добраться до конца главы 1, 2 или даже 3,
прежде чем вы узнаете, как вывести на экран «Hello World». Мы же
перейдем к этому вопросу немедленно! Впрочем, поскольку вы уже
писали код на bash и храните его в системе управления версиями
(верно?), то говорить об echo 'Hello, World' было бы довольно глупо,
а потому и не будем. Упс.

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

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

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

1
2

https://github.com/vossenjp/bashidioms-examples/blob/main/bcb2-appd.pdf.
https://oreil.ly/fPHy8.

Использование исходного кода примеров  

15

Моноширинный курсив

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

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

Этот рисунок указывает на общее примечание.

Этот рисунок указывает на предупреждение.

Использование исходного кода примеров
Вспомогательные материалы (примеры кода, упражнения и т. д.) доступны для загрузки по адресу: https://github.com/vossenjp/bashidioms-examples.
Если у вас возникнут вопросы технического характера по использованию примеров кода, направляйте их по электронной почте на адрес
bookquestions@oreilly.com.
В общем случае все примеры кода из книги вы можете использовать
в своих программах и в документации. Вам не нужно обращаться

16  Вступление

в издательство за разрешением, если вы не собираетесь воспроизводить
существенные части программного кода. Если вы разрабатываете программу и используете в ней несколько фрагментов кода из книги, вам
не нужно обращаться за разрешением. Но для продажи или распространения примеров из книги вам потребуется разрешение от издательства
O’Reilly. Вы можете отвечать на вопросы, цитируя данную книгу или
примеры из нее, но для включения существенных объемов программного
кода из книги в документацию вашего продукта потребуется разрешение.
Мы рекомендуем, но не требуем добавлять ссылку на первоисточник
при цитировании. Под ссылкой на первоисточник мы подразумеваем
указание авторов, издательства и ISBN.
За получением разрешения на использование значительных объемов
программного кода из книги обращайтесь по адресу permissions@oreilly.com.

Благодарности

Bash
Спасибо GNU Software Foundation и Брайану Фоксу (Brian Fox) за
создание bash. Благодарим также Чета Рэми (Chet Ramey), поддерживающего и совершенствующего bash, начиная с версии 1.14, которая вышла в первой половине 1990-х. Все вы дали нам отличный инструмент.

Рецензентам
Большое спасибо рецензентам: Дугу Макилрою (Doug McIlroy), Яну
Миеллу (Ian Miell), Кертису Олду (Curtis Old) и Полу Тронконе (Paul
Troncone), которые помогли значительно улучшить книгу! Все они дали
ценные отзывы, а в некоторых случаях предложили альтернативные решения, указав на проблемы, которые мы упустили из виду. Если в этой
книге есть ошибки или упущения, то это не их, а наша вина.

O’Reilly
Спасибо всем сотрудникам издательства O’Reilly, без которых эта книга не появилась бы на свет, а если и появилась бы, то по содержанию
и оформлению была бы беднее.
Спасибо Майку Лукидесу (Mike Loukides) за оригинальную идею и то,
что предложил и доверил реализовать ее нам. Спасибо Сюзанне «Зан»

18  Благодарности

МакКуэйд (Suzanne «Zan» McQuade), что помогла воплотить идею
в жизнь. Огромная благодарность Николь Таше (Nicole Taché) и Кристен
Браун (Kristen Brown) за правки и то, что терпели нас в ходе долгой работы
над книгой и ее подготовки к печати. Особое спасибо Нику Адамсу (Nick
Adams) из Tools за исправление наших многочисленных (и порой вопиющих) ошибок в AsciiDoc, а также помощь в вопросах, не связанных с набором. Спасибо литературному редактору Ким Сандовал (Kim Sandoval),
составителю индекса Шерил Ленсер (Cheryl Lenser), корректору Лиз
Уилер (Liz Wheeler), дизайнерам Дэвиду Футато (David Futato) и Карен
Монтгомери (Karen Montgomery), а также другим сотрудникам O’Reilly.

От Карла
Спасибо Джей Пи за его работу, внимание к деталям и готовность к сотрудничеству. Спасибо всем сотрудникам O’Reilly за помощь в издании
этой книги.
Эту книгу я посвящаю моей супруге Синтии, которая мирилась с моими
писательскими амбициями и достаточно убедительно делала вид, что ей
интересно, о чем я пишу. Моя работа над этой книгой направлена, как говорят в Бетельском университете, во славу Божию и на благо моих ближних.

От Джей Пи
Спасибо Карлу за его работу; похоже, нам снова удалось уложиться
в график. Спасибо Майку за то, что опять привел все в движение, и Николь за ее труд, а также терпеливое отношение к нашим работе, жизни
и неумению правильно распределять время.
Эту книгу я посвящаю моей супруге Карен — «исполнительному вицепрезиденту», отвечающему за все. Спасибо за твою невероятную поддержку, терпение и понимание, без тебя я бы не состоялся в жизни. Наконец,
спасибо Кейт и Сэму за то, что нормально воспринимали мой ответ: «Если
вы не истекаете кровью и не горите, то не мешайте: я занят книгой».

От издательства

Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com
(издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.

ГЛАВА 1

Идиома «большого» if

Знакомство с идиомами bash мы начнем с конструкции, которая позволяет делать то же самое, что и привычные операторы if/then/else, но имеет
более компактный синтаксис. Идиоматическая конструкция, которую
мы рассмотрим в этой главе, не только дает реальные преимущества
(в основном — краткость), но также таит некоторые ловушки. Кроме
того, не зная этой идиомы bash, можно вообще не понять смысл кода.
Взгляните на фрагмент кода:
[[ -n "$DIR" ]] && cd "$DIR"

Как вы думаете, похож он на оператор if? Знакомые с bash заметят,
что функционально это тождественно оператору if, даже при том, что
ключевое слово if отсутствует.
Давайте разберем, что делает этот код.

«Большой» if
Прежде чем объяснить эту идиому, рассмотрим похожий, но более простой пример:
cd tmp && rm scratchfile

По сути, это тоже оператор if. Если команда cd выполнится успешно, то
выполнится и команда rm. «Идиома» здесь — это использование для разделения команд пары амперсандов (&&), которая обычно читается как «И».

«Большой» if  

21

На уроках логики и философии учат правилу: выражение «А И Б» истинно тогда и только тогда, когда оба условия, А и Б, истинны. Следовательно, если А ложно, то нет необходимости даже рассматривать Б.
Например, возьмем такое выражение: «У меня есть собака, И у меня есть
кошка». Если у меня нет собаки, то это составное выражение неверно,
независимо от наличия у меня кошки.
Применим это правило в bash. Напомним, что основная функция bash —
выполнять команды. В первой части нашего примера выполняется команда cd. В соответствии с логикой, если эта первая команда потерпела
неудачу, то bash не будет выполнять вторую команду rm.
Оператор && позволяет использовать логическое «И». На самом деле bash
не выполняет логическую операцию с двумя результатами (в C/C++ была
бы другая логика при таком же синтаксисе). Эта идиома просто обеспечивает условное выполнение второй команды, которая не запускается,
если первая команда завершилась с ошибкой.
Теперь вернемся к исходному примеру:
[[ -n "$DIR" ]] && cd "$DIR"

Стал ли он теперь более понятным? Первое выражение проверяет, отличается ли длина значения переменной DIR от нуля. Если переменная
имеет некоторое значение, то есть длина этого значения отлична от нуля,
то команда cd попытается перейти в каталог, имя которого соответствует
значению DIR.
То же самое можно было бы записать, явно использовав оператор if:
if [[ -n "$DIR" ]]; then
cd "$DIR"
fi

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

22  Глава 1. Идиома «большого» if

и насколько высока вероятность добавления других команд в ветку then.
В следующих разделах мы рассмотрим несколько возможных вариантов.
Справка bash
Команда help в bash служит отличным источником информации о встроенных командах, а команда help test дает ценные подсказки о выражениях проверки условий, таких как -n. Также можно заглянуть в справочное руководство man bash, тогда придется отыскать раздел Conditional
expressions (условные выражения). Но справочник man — очень объемный,
а команда help дает короткие и содержательные подсказки. Если вы не
уверены, встроена ли та или иная команда в bash, то просто передайте ее
команде help или введите: type -a команда.
Если бы вы знали, что help test расскажет вам о значении -n, то, может,
и не купили бы эту книгу. И еще один тонкий момент: попробуйте команду
help [. Пожалуйста.

Или ELSE...
Существует похожая идиома, основанная на использовании символов ||
для разделения двух элементов в команде bash. Эта пара символов читается как «ИЛИ»: вторая часть команды будет выполнена, только если
первая закончится неудачей. Такой образ действий напоминает логическое «ИЛИ», например: А ИЛИ Б. Все выражение истинно, если истинна
хотя бы одна из его частей, А или Б. Иными словами, если А истинно,
то не имеет значения, истинно ли Б. Например, рассмотрим такое выражение: «У меня есть собака ИЛИ кошка». Если у меня действительно
есть собака, то это выражение истинно, независимо от наличия кошки.
Применим это правило в bash:
[[ -z "$DIR" ]] || cd "$DIR"

Сможете объяснить, как работает это выражение? Если значение переменной имеет нулевую длину, то первая часть будет оценена как «истинная» и вторая половина выполняться не будет, то есть команда cd

Выполняем несколько команд  

23

не будет выполнена. Но если значение $DIR имеет ненулевую длину, то
проверка вернет «ложь» и команда cd выполнится.
Эту строку на языке bash можно прочитать так: «Либо $DIR имеет нулевую длину, либо попытаться перейти в этот каталог».
Запись тех же действий с использованием оператора if выглядит немного странно, потому что ветка then — пустая. Код после || похож на
ветку else:
if [[ -z "$DIR" ]]; then
:
else
cd "$DIR"
fi

Двоеточие (:) — это пустая инструкция, которая ничего не делает.
Итак, две команды, разделенные символами &&, похожи на оператор if
и его ветку then; две команды, разделенные символами ||, похожи на
оператор if и его ветку else.

Выполняем несколько команд
Если требуется выполнить несколько команд после пары символов ||,
как в ветке else, или после &&, как в ветке then, то нередко допускаются
ошибки. Например, может возникнуть соблазн написать такой код:
# Внимание: этот код работает не так, как можно предположить!
cd /tmp || echo "cd to /tmp failed." ; exit

Оператор «ИЛИ» говорит нам, что в случае сбоя cd выполнится команда
echo, которая сообщит пользователю, что cd потерпела неудачу. Но вот в чем
загвоздка: exit выполнится в любом случае. Вы этого не ожидали, верно?
Интерпретируйте точку с запятой (;) как эквивалент перевода строки,
и все сразу станет на свои места (и выяснится, что это не то, чего вы
хотели):

24  Глава 1. Идиома «большого» if

cd /tmp || echo "cd to /tmp failed."
exit

Можно ли добиться желаемого результата? Да, для этого следует
сгруппировать echo и exit в одно предложение справа от «ИЛИ»,
например:
# Или cd выполнится успешно, или сценарий завершится с сообщением об ошибке
cd /tmp || { echo "cd to /tmp failed." ; exit ; }

Фигурные скобки в bash используются для определения составных
команд, то есть для группировки инструкций. Возможно, вы видели
нечто подобное с использованием круглых скобок, но инструкции,
заключенные в круглые скобки, выполняются в подоболочке, также
называемой дочерним процессом. Это связано с ненужными в данном
случае расходами ресурсов, к тому же выход по команде exit произойдет
из подоболочки, что не даст желаемого результата.
Завершение составных команд
Синтаксис bash требует в обязательном порядке завершать составные
команды точкой с запятой или переводом строки перед закрывающей
фигурной скобкой. Если используется точка с запятой, то она должна
отделяться пробелом от закрывающей фигурной скобки, чтобы интерпретатор распознал ее как служебный символ (иначе она будет
перепутана с закрывающей фигурной скобкой синтаксиса переменных
оболочки, например ${VAR}). Вот почему предыдущий пример заканчивается, казалось бы, лишней точкой с запятой: { echo "..." ; exit ; }.
При использовании перевода строки завершающая точка с запятой не
нужна:
# Или cd выполнится успешно, или сценарий завершится с сообщением
об ошибке
cd /tmp || { echo "cd to /tmp failed." ; exit
}

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

Так делать не нужно!  

25

Еще о случае нескольких команд
Что, если требуется реализовать более сложную логику, например с несколькими конструкциями «И» и «ИЛИ»? Как их объединить? Рассмотрим следующую строку кода:
[ -n "$DIR" ] && [ -d "$DIR" ] && cd "$DIR" || exit 4

Если переменная DIR — непустая и существует каталог с таким именем,
то cd выполнит переход в этот каталог; иначе сценарий завершится с кодом 4. Эта группа команд делает именно то, что можно было бы ожидать,
но правильно ли вы понимаете логику?
При взгляде на этот пример можно подумать, что оператор && имеет
более высокий приоритет, чем ||, но в действительности это не так. Они
выполняются в порядке следования слева направо. В bash операторы &&
и || имеют одинаковый приоритет и являются левоассоциативными.
Хотите доказательств? Взгляните на следующие примеры:
# Пример 1
$ echo 1 && echo 2 || echo 3
1
2
$
# Пример 2
$ echo 1 || echo 2 && echo 3
1
3
$

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

Так делать не нужно!
Пока мы не ушли далеко от главной темы главы, рассмотрим примеры использования оператора if, которые типичны для сценариев, написанных

26  Глава 1. Идиома «большого» if

много лет назад. Мы показываем их, чтобы дополнительно пояснить
идиому «большого» if, а также убедить вас никогда не подражать этому
стилю. Итак, вот код:
### Не используйте операторы if для таких проверок
if [ $VAR"X" = X ]; then
echo empty
fi
### Или таких
if [ "x$VAR" == x ]; then
echo empty
fi
### И других, подобных им

Здесь всего лишь выполняется проверка, не является ли переменная VAR
пустой. Для этого к ее значению добавляется некоторый символ (в этих
примерах X и x), и, если в результате получается строка, совпадающая
только с этим символом, значит, переменная имеет пустое значение.
Не делайте так. Есть лучшие способы выполнить такую проверку. Вот
простая альтернатива:
# Значение переменной имеет нулевую длину?
if [[ -z "$VAR" ]]; then
echo empty
fi

Одиночные и двойные квадратные скобки
В примерах кода выше проверяемое условие заключено в одиночные
квадратные скобки [ ]. Но главная проблема не в них. В первую очередь,
мы рекомендуем избегать приема с добавлением значения и сравнением
строк — для таких проверок используйте ключи -z или -n. Почему же
в других наших примерах в операторах if и заменяющих их конструкциях используются двойные квадратные скобки [[ и ]]? Они являются
дополнением, появившимся в bash и отсутствующим в оригинальной командной оболочке sh. Благодаря этому исключаются некоторые чреватые
ошибками ситуации, например когда имя переменной в одних случаях
заключено в кавычки, а в других — нет. Мы использовали в двух примерах выше одиночные квадратные скобки, потому что код такого вида
часто встречается в старых сценариях. Возможно, вам придется использовать одиночные скобки, если приоритетом является совместимость
между различными платформами, в том числе не поддерживающими bash

В заключение: стиль и удобочитаемость  

27

(например, использующими dash). Дополнительно отметим, что двойные
квадратные скобки — это ключевые слова, тогда как левая одиночная
квадратная скобка — это встроенная функция. Это объясняет некоторые
тонкие отличия в их свойствах. Мы советуем по возможности всегда использовать двойные квадратные скобки.

В случае, когда длина значения переменной или строки не равна нулю,
можно использовать ключ -n или просто сослаться на переменную:
# Проверяет, что значение переменной имеет ненулевую длину
if [[ -n "$VAR" ]]; then
echo "VAR has a value:" $VAR
fi
# То же самое
if [[ "$VAR" ]]; then
echo even easier this way
fi

Как видите, нет необходимости использовать подход, который был нужен в устаревших версиях команды test («[»). Однако мы посчитали,
что вы должны знать о нем, чтобы понимать старые сценарии. Кроме
того, теперь вы знаете лучший способ добиться того же результата.

В заключение: стиль и удобочитаемость
В этой главе мы рассмотрели важную идиому bash — проверку условия
без оператора if. Такая проверка не похожа на традиционную конструкцию if/then/else, но позволяет получать те же результаты. Если не знать
о ней, то некоторые сценарии могут остаться для вас неясными. Эту идиому целесообразно использовать для проверки всех предварительных
условий перед выполнением команды или быстрой проверки ошибок,
не нарушая основной логики сценария.
С помощью операторов && и || можно реализовать логику if/then/else.
Но в bash есть также и ключевые слова if, then и else, поэтому возникает
вопрос: когда использовать их, а когда сокращенные конструкции? Ответ: все зависит от удобочитаемости.

28  Глава 1. Идиома «большого» if

Для определения сложной логики лучше использовать знакомые ключевые слова. А для простых проверок с одиночными командами часто
удобнее операторы && и ||, потому что они не отвлекают внимание от
основного алгоритма. Используйте help test, чтобы вспомнить, какие
проверки выполняют, например, ключи -n и -r , и скопируйте текст
справки в памятку на будущее.
В любом случае и в знакомых операторах if, и в идиоматических проверках без if мы рекомендуем использовать синтаксис с двойными
квадратными скобками.
Теперь, подробно рассмотрев одну идиому, давайте взглянем на другие,
чтобы поднять на новый уровень ваши умения программировать на bash.

ГЛАВА 2

Язык циклов

В bash имеются не только циклы for в стиле C, но также другие виды
и стили организации циклического выполнения инструкций. Некоторые
из них ближе программистам на Python, но у каждого есть свои особые
задачи. Существует цикл for без очевидных аргументов, который удобно
использовать как в сценариях, так и внутри функций. Также есть похожий на итератор цикл for, значения для которого могут задаваться
явно или возвращаться другими командами.

Циклические конструкции
Циклические конструкции распространены в языках программирования. С момента изобретения языка C многие языки программирования
заимствовали из него цикл for. Это мощная и понятная конструкция,
объединяющая код инициализации, условие завершения и код, выполняемый в начале каждой итерации. В C, Java и многих других языках
цикл for выглядит следующим образом:
/* НЕ bash */
for (i=0; i $BOOK_ASC/index.txt && {
echo "Updated: $BOOK_ASC/index.txt"
exit 0
} || {
echo "FAILED to update: $BOOK_ASC/index.txt"
exit 1
}
;;
rerun ) ## Запуск примеров для повторного создания (существующих!)
## выходных файлов
# Запускать только для кода, для которого УЖЕ ИМЕЕТСЯ файл *.out...,
# но не для ВСЕГО кода *.sh
for output in examples/*/*.out; do
code=${output/out/sh}
echo "Re-running code for: $code > $output"
$code > $output
done
;;

Сценарии-обертки  51

cleanup )
## Очистка мусора xHTML/XML/PDF
rm -fv {ch??,app?}.{pdf,xml,html} book.{xml,html} docbook-xsl.css
;;
* )

;;



\cd - # НЕУКЛЮЖИЙ способ отменить команду 'cd' выше...
( echo "Usage:" ⓮
egrep '\)[[:space:]]+# ' $0 ⓯
echo ''
egrep '\)[[:space:]]+## ' $0 ⓰
echo ''
egrep '\)[[:space:]]+### ' $0 ) | grep "${1:-.}" | more



esac

В этом сценарии происходит много интересного, поэтому разберем его
подробнее:

❶ Сценарий выполняет множество операций с исходным кодом форма-

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

❷ Обычно для хранения базового имени мы используем переменную
$PROGRAM,

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

❸ Как мы обсудим подробнее в главе 11, использование осмысленных

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

❹ После сохранения операции, которую требуется выполнить, нам

больше не нужен параметр $1, но в командной строке могут быть переданы другие параметры, поэтому сдвинем их командой shift на одну
позицию влево.

❺ Если файл /usr/bin/xsel существует, имеет разрешение на выполне-

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

52  Глава 3. На всякий случай: оператор Case

❻ Именно здесь начинается реальная работа. Прежде всего определяем,
какое «действие» задано.
❼ Для удобочитаемостикода разделяем его на функциональные блоки
(см. также п. ⓫).
❽ Первый раздел — обработка разметки заголовков.
❾ Строка является одновременно и кодом, и документацией. Действие
заключается в оформлении заголовка верхнего уровня h1 (для кода
книги). Позже мы увидим связь с документацией.
❿ Выполняем обработку. Сначала рекурсивно вызываем сценарий,

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

⓫ Для удобочитаемости кода разделяем его на функциональные блоки
(см. также п. ❼).
⓬ Длинные строки можно переносить, добавляя символ \ (см. также
раздел «Форматирование» в главе 11).
⓭ Далее следует еще одна порция интересного кода в варианте по

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

⓮ Мы заключаем выходные данные в подоболочку, которую по конвейеру передаем команде more на случай, если вывод окажется слишком
длинным.

⓯ Строка кода как документация, о которой мы говорили в п. ❾. С по-

мощью команды egrep мы извлекаем закрывающую скобку ) из нашего оператора case, за которой следуют пробелы и один маркер начала комментария # . В результате мы получаем заголовок первого

Сценарии-обертки  53

функционального блока «Содержимое/разметка». Эта операция извлекает фактические строки кода, составляющие оператор case, и объясняет
их действия благодаря комментариям.

⓰ Описанное в п. ⓯ действие выполняется для второго блока «Инструменты».

⓱ Описанное в п. ⓯ действие выполняется для третьего блока, об-

рабатывающего операции с репозиторием Git (мы опустили этот
код для простоты). Здесь дополнительно используется команда grep
с параметром ${1:-.}, чтобы показать либо подсказку по запросу, например wrapper.sh help heading, либо полный текст справки (grep ".").
В коротком сценарии это может показаться не особенно нужным, но,
поскольку он со временем дополняется, такая возможность становится
действительно удобной!
Результатом команд grep является отображение справочного сообщения,
отсортированного и сгруппированного по разделам:
$ examples/ch03/wrapper.sh help
Usage:
h1 )
# Заголовок уровня 1 (в AsciiDoc h3)
h2 )
# Заголовок уровня 2 (в AsciiDoc h4)
h3 )
# Заголовок уровня 3 (в AsciiDoc h5)
bul|bullet )
# Маркированный список (** = уровень 2, + =
# многострочный)
nul|number|order* ) # Нумерованный список (.. = уровень 2, + = многострочный)
term )
# Термины
bold )
# Полужирный
i|italic*|itl )
# Курсив
c|constant|cons )
# Моноширинный (команды, код, ключевые слова и др.)
type|constantbold ) # Полужирный моноширинный (ввод пользователя)
var|constantitalic ) # Курсив моноширинный (пользовательские значения)
sub|subscript )
# Нижний индекс
sup|superscript )
# Верхний индекс
foot )
# Сноска
url|link )
# URL с альтернативным текстом
esc|escape )
# Экранирующий символ (например, *)
id )
## Преобразовать имя рецепта в ID
index )
## Создать 'index.txt' в каталоге AsciiDoc
rerun )
## Запуск примеров для повторного создания
## (существующих!) выходных файлов
cleanup )
## Очистка мусора xHTML/XML/PDF

54  Глава 3. На всякий случай: оператор Case

$ examples/ch03/wrapper.sh
h1 )
#
h2 )
#
h3 )
#

help heading
Заголовок уровня 1 (в AsciiDoc h3)
Заголовок уровня 2 (в AsciiDoc h4)
Заголовок уровня 3 (в AsciiDoc h5)

Еще один важный момент
Каждый вариант в операторе case мы завершаем двойной точкой с запятой. В первом примере в начале этой главы мы написали:
"yes") echo "glad you agreed" ;;

Символы ;; означают, что никаких дальнейших действий предпринимать не нужно. Встретив эту пару символов, bash продолжит выполнение
сценария с первой инструкции после ключевого слова esac.
Но такое поведение нежелательно, если требуется продолжить проверку других вариантов в операторе case или выполнить иные действия.
Синтаксис bash позволяет это сделать с помощью комбинаций символов
;;& и ;&.
Вот пример такого кода для получения подробной информации о пути
в $filename:
case $filename in
./*) echo -n "local"
;&
[^/]*) echo -n "relative"
;;&
/*) echo -n "absolute"
;&
*/*) echo "pathname"
;;
*) echo "filename"
;;
esac

# Начинается с ./
# Спуститься к следующему варианту
# Начинается с любого символа, кроме слеша
# Проверить совпадения с другими вариантами
# Начинается с символа слеша
# Спуститься к следующему варианту
# В имени есть слеш
# Завершить
# Все остальные случаи
# Завершить

В заключение: стиль и удобочитаемость  

55

Шаблоны в приведенных выше вариантах будут сравниваться по порядку со значением в $filename. Первый шаблон состоит из двух буквенных
символов — точки и слеша, за которыми следуют любые символы. Если
обнаружится совпадение с этим шаблоном (например, если в $filename
передано значение ./this/file), то сценарий выведет «local», но без перевода строки в конце. Следующая строка в сценарии (;&) предписывает
спуститься дальше и выполнить команды, перечисленные в следующем
варианте (без проверки совпадения с шаблоном). В результате сценарий
выведет слово «relative». В отличие от предыдущего шаблона, этот раздел
кода заканчивается символами ;;&, требующими проверить совпадение
с другими шаблонами (по порядку).
Поэтому далее будет проверено наличие косой черты в начале $filename.
Если значение не совпадет с этим шаблоном, то будет проверен следующий шаблон — символ косой черты в любом месте в строке (любое
количество любых символов, затем косая черта, затем опять любое количество любых символов). Если совпадение с этим шаблоном обнаружится (а в нашем примере это так), сценарий выведет слово «pathname».
Символы ;; в конце указывают, что в проверке последующих шаблонов
нет необходимости, — и bash завершит выполнение оператора case.

В заключение: стиль и удобочитаемость
В этой главе мы описали оператор case, поддерживающий возможность множественного ветвления. Возможность сопоставления с образцом делает его очень полезным для разработки сценариев, хотя
чаще он используется для определения простого совпадения определенных слов.
Последовательности символов ;;, ;;& и ;& добавляют дополнительные
возможности, но их применение может вызывать сложности. Возможно,
конструкция if/then/else позволит неопытным пользователям лучше
структурировать такую логику.

56  Глава 3. На всякий случай: оператор Case

Символы ;;& и ;& имеют настолько тонкие различия, что есть опасность неправильно оценить последствия их использования. Поток
управления после сопоставления с шаблоном может быть различным
в каждом случае: «спускаться ниже» и выполнять дополнительный
код, пытаться отыскать совпадение с другим шаблоном или завершить
выполнение. Поэтому мы настоятельно рекомендуем снабжать такие
строки сценария подробными комментариями, чтобы избежать путаницы или недопонимания.

ГЛАВА 4

Язык переменных

Нередко можно увидеть сообщение об ошибке или инструкцию присваивания с идиомой ${0##*/}, которая выглядит как ссылка на $0, но в действительности представляет собой нечто большее. Давайте рассмотрим
подробнее ссылки на переменные и используемые в них дополнительные
символы. Вы узнаете, что за этими несколькими специальными символами кроется целый набор операций со строками с весьма широкими
возможностями.

Ссылка на переменную
В большинстве языков программирования ссылка на значение переменной выглядит очень просто. Как правило, нужно просто указать имя
переменной, но в некоторых языках к нему добавляется определенный
символ, сообщающий, что требуется получить значение. К таким языкам
относится и bash: если присваивание выполняется по имени переменной,
VAR=something, то получение значения — по имени с префиксом в виде
знака доллара, $VAR. Зачем нужен знак доллара? Так как bash в основном
работает со строками, выражение
MSG="Error: FILE not found"

даст вам простую строку из четырех слов, тогда как
MSG="Error: $FILE not found"

58  Глава 4. Язык переменных

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

Чтобы избежать путаницы с определением места, где заканчивается
имя переменной (в рассмотренном выше примере пробелы упрощают
задачу), следует использовать полный синтаксис — фигурные скобки
вокруг имени переменной, ${FILE}.
Применение фигурных скобок служит основой для многих специальных
синтаксических конструкций, связанных со ссылками на переменные.
Например, можно поставить решетку перед именем переменной ${#VAR},
чтобы вернуть не значение, а его длину в символах.

${VAR}

${#VAR}

oneword

7

/usr/bin/longpath.txt

21

many words in one string

24

3

1

2356

4

1427685

7

Но bash может не только извлекать значение или выводить его длину.

Дополнительные параметры  

59

Дополнительные параметры
При извлечении значения переменной можно задать правила подстановки или правки, влияющие на возвращаемое значение, но не на значение
в переменной (за исключением одного случая). Для этого используются
специальные последовательности символов внутри фигурных скобок,
ограничивающих имя переменной, например ${VAR##*/}. Рассмотрим
несколько таких последовательностей, о которых следует знать.

Сокращенный вариант команды basename
Чтобы запустить сценарий, можно использовать одно только имя его
файла, но при этом требуется, чтобы файл имел разрешение на выполнение и находился в каталоге, включенном в список для поиска файлов
в переменной окружения PATH. Сценарий можно запустить командой
./scriptname, если он находится в текущем каталоге. Можно указать
полный путь, например /home/smith/utilities/scriptname, или относительный, если каталог со сценарием располагается недалеко от текущего
рабочего каталога.
Независимо от способа вызова, $0 будет хранить последовательность
символов, использованную для запуска сценария, — относительный
или абсолютный путь.
Для идентификации сценария в сообщении о порядке использования
часто достаточно базового имени — имени самого файла без пути к нему:
echo "usage: ${0##*/} namesfile datafile"

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

60  Глава 4. Язык переменных

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

Удаление пути или префикса
Чтобы удалить символы в начале (слева) или конце (справа) строки,
нужно добавить к ссылке на переменную символ # и шаблон, соответствующий удаляемым символам. Выражение ${MYVAL#img_} удалит
символы img_, если с них начинается значение переменной MYVAL. Более
сложный шаблон, ${MYVAL#*_}, удалит любую последовательность символов до подчеркивания включительно. Если же совпадения с шаблоном
не обнаружится, то выражение вернет полное значение без изменений.
Если использовать один символ #, будет удалено кратчайшее из возможных совпадений. При использовании пары символов ## будет удалено
самое длинное совпадение.
Теперь, возможно, вы понимаете, что делает выражение ${0##*/}? Из значения в переменной $0 — пути к файлу сценария — слева будет удалена самая
длинная последовательность любых символов, заканчивающаяся косой
чертой, т. е. все компоненты пути, и останется только имя самого сценария.
Ниже представлены несколько возможных значений $0 и результаты
применения двух видов шаблона, чтобы показать, какое влияние на
результат оказывают требования удаления кратчайшего (#) и самого
длинного (##) совпадений:
Значение $0

Выражение

Возвращаемый результат

./ascript

${0#*/}

ascript

./ascript

${0##*/}

ascript

../bin/ascript

${0#*/}

bin/ascript

../bin/ascript

${0##*/}

ascript

/home/guy/bin/ascript

${0#*/}

home/guy/bin/ascript

/home/guy/bin/ascript

${0##*/}

ascript

Дополнительные параметры  

61

Обратите внимание, что шаблон с кратчайшим совпадением для */ может
соответствовать, в том числе, только косой черте.
Шаблоны командной оболочки — не регулярные выражения
Шаблоны, используемые при извлечении значения переменной, не являются регулярными выражениями. В шаблонах командной оболочки *
соответствует нулевому или большему количеству символов, ? — одному
символу, а [символы] — любому из символов внутри фигурных скобок.

Сокращенный вариант команды dirname
или удаление суффикса
Подобно тому как # удаляет префикс, то есть символы слева, знак % удаляет суффикс, то есть символы справа. Двойной знак процента удаляет
самое длинное совпадение. Рассмотрим несколько примеров удаления
суффикса. В первых примерах используется переменная $FN, содержащая имя файла изображения. Это имя может заканчиваться расширением .jpg, .jpeg, .png или .gif. Сравните, как разные шаблоны удаляют
различные части строки. В последних нескольких примерах показано,
как получить нечто похожее на результат применения команды dirname
к параметру $0:
Значение переменной

Выражение

Возвращаемый результат

img.1231.jpg

${FN%.*}

img.1231

img.1231.jpg

${FN%%.*}

img

./ascript

${0%/*}

.

./ascript

${0%%/*}

.

/home/guy/bin/ascript

${0%/*}

/home/guy/bin

/home/guy/bin/ascript

${0%%/*}

Это выражение не всегда возвращает тот же результат, что и команда
dirname. Например, применительно к значению /file наше выражение

62  Глава 4. Язык переменных
вернет пустую строку, а dirname — косую черту. При желании этот случай можно отдельно обработать в сценарии, добавив дополнительную
логику. Также можно игнорировать его, если предполагается, что он
никогда не встретится, или просто добавить косую черту в конец выражения, например ${0%/*}/ , чтобы результат всегда заканчивался
косой чертой.
Удаление префикса и суффикса
Чтобы проще запомнить, что # удаляет совпадение слева, а % — справа, посмотрите на клавиатуру: символ # (Shift+3) находится слева от % (Shift+5).

Другие модификаторы
Помимо # и %, есть еще несколько модификаторов, способных изменить значение, которое извлекается из переменной. С помощью ^ или
^^ можно преобразовать, соответственно, первый или все символы
в строке в верхний регистр, а с помощью , или ,, — в нижний регистр,
как показано ниже:
Значение переменной TXT

Выражение

Возвращаемый результат

message to send

${TXT^}

Message to send

message to send

${TXT^^}

MESSAGE TO SEND

Some Words

${TXT,}

some Words

Do Not YELL

${TXT,,}

do not yell

Можно также использовать специальные объявления переменных:
declare -u UPPER и declare -l lower. Содержимое переменных, объявленных таким образом, всегда будет преобразовываться в верхний или
нижний регистр соответственно.
Но самым гибким является модификатор /. Он выполняет замену в любом месте строки, а не только в начале или конце. Подобно команде sed,

Дополнительные параметры  

63

этот модификатор позволяет определить искомый шаблон, а с помощью
дополнительной косой черты — строку замены. Одиночная косая черта
означает одну замену (первое совпадение), а две косые черты — замену
всех совпадений. Вот несколько примеров:
Значение переменной FN

Выражение

Возвращаемый результат

FN="my filename with spaces.txt" ${FN/ /_}

my_filename with spaces.txt

FN="my filename with spaces.txt" ${FN// /_}

my_filename_with_spaces.txt

FN="my filename with spaces.txt" ${FN// /}

myfilenamewithspaces.txt

FN="/usr/bin/filename"

${FN//\// } usr bin filename

FN="/usr/bin/filename"

${FN/\// }

usr/bin/filename

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

Почему бы не использовать этот гибкий механизм замены всегда? Зачем
обременять себя изучением модификаторов # и %, удаляющих символы
в начале и конце строки? Давайте рассмотрим такое имя файла: frank.
gifford.gif и предположим, что нам нужно конвертировать его в формат
jpg с помощью команды convert из пакета Image Magick. Однако замена
с использованием / не позволяет привязать поиск к определенной части строки. Поэтому, если попытаться заменить .gif в имени файла на
.jpg, то вы получите frank.jpgford.gif. В подобных ситуациях удаление
суффикса с помощью % существенно облегчит задачу.
Еще один полезный модификатор извлекает подстроку из переменной. Добавьте двоеточие после имени переменной, укажите смещение
вправо первого символа извлекаемой подстроки (отсчет смещений начинается с 0, то есть 0 соответствует первому символу строки), добавьте
еще одно двоеточие и укажите длину извлекаемой подстроки. Если

64  Глава 4. Язык переменных

опустить второе двоеточие и длину, то выражение вернет оставшуюся
часть строки. Вот несколько примеров:
Значение переменной FN

Выражение

Возвращаемый результат

/home/bin/util.sh

${FN:0:1}

/

/home/bin/util.sh

${FN:1:1}

h

/home/bin/util.sh

${FN:3:2}

me

/home/bin/util.sh

${FN:10:4}

util

/home/bin/util.sh

${FN:10}

util.sh

Пример 4.1 демонстрирует использование дополнительных параметров
для анализа входных данных и обработки определенных полей, которые
будут использоваться при автоматическом создании правил брандмауэра. Мы также включили в код большую справочную таблицу дополнительных параметров bash, так как заботимся об удобочитаемости кода.
Результаты выполнения кода показаны в примере 4.2.
Пример 4.1. Анализ входных данных с использованием дополнительных
параметров: код
#!/usr/bin/env bash
# parameter-expansion.sh: применение дополнительных параметров для анализа
# Автор и дата: _bash Idioms_ 2022
# Имя файла в bash Idioms: examples/ch04/parameter-expansion.sh
#_________________________________________________________________________
# Не работает в Zsh 5.4.2!
customer_subnet_name='Acme Inc subnet 10.11.12.13/24'
echo ''
echo "Say we have this string: $customer_subnet_name"
customer_name=${customer_subnet_name%subnet*}
subnet=${customer_subnet_name##* }
ipa=${subnet%/*}
cidr=${subnet#*/}
fw_object_name=${customer_subnet_name// /_}
fw_object_name=${fw_object_name////-}
fw_object_name=${fw_object_name,,}

#
#
#
#
#
#
#

Удалить 'subnet' в конце
Удалить начальные пробелы
Удалить '/*' в конце
Удалить до '/*'
Заменить пробелы на '_'
Заменить '/' на '-'
В нижний регистр

Дополнительные параметры  

echo
echo
echo
echo
echo
echo
echo
echo

65

''
'When the code runs we get:'
''
"Customer name: $customer_name"
"Subnet:
$subnet"
"IPA
$ipa"
"CIDR mask:
$cidr"
"FW Object:
$fw_object_name"

# Дополнительные параметры в bash: https://oreil.ly/Af8lw
#
#
#
#

${var#pattern}
${var##pattern}
${var%pattern}
${var%%pattern}

Удалить
Удалить
Удалить
Удалить

кратчайшее совпадение с
самое длинное совпадение
кратчайшее совпадение с
самое длинное совпадение

pattern в
с pattern
pattern в
с pattern

начале
в начале
конце
в конце

# ${var/pattern/replacement} Заменить первое совпадение с pattern на replacement
# ${var//pattern/replacement} Заменить все совпадения с pattern на replacement
#
#
#
#
#
#
#
#

${var^pattern}
${var^^pattern}
${var,pattern}
${var,,pattern}

Преобразовать
регистр
Преобразовать
регистр
Преобразовать
регистр
Преобразовать
регистр

первое совпадение с pattern в верхний
все совпадения с pattern в верхний
первое совпадение с pattern в нижний
все совпадения с pattern в нижний

# ${var:offset}
# ${var:offset:length}

Извлечь подстроку, начиная с offset
Извлечь подстроку, начиная с offset, длиной length

#
#
#
#
#

${var:-default}
${var:=default}

Вернуть значение var, если имеется, иначе default
Присвоить default переменной var, если она еще не
установлена
Вернуть error_message, если var не установлена
Вернуть replaced, если var установлена

#
#
#
#
#
#
#
#
#
#
#
#
#

${#var}
${!var[*]}
${!var[@]}

${var:?error_message}
${var:+replaced}

${!prefix*}
${!prefix@}
${var@Q}
${var@E}
${var@P}
${var@A}
${var@a}

Вернуть длину var
Вернуть индексы или ключи массива
Вернуть индексы или ключи массива (поддерживаются
кавычки)
Вернуть имена переменных, начинающиеся с +prefix+
Вернуть имена переменных, начинающиеся с prefix,
(поддерживаются кавычки)
Вернуть значение в кавычках
Вернуть развернутое значение (лучше, чем `eval`!)
Вернуть развернутое значение как приглашение к вводу
Вернуть оператор присваивания или объявления
переменной
Вернуть атрибуты

66  Глава 4. Язык переменных

Пример 4.2. Анализ входных данных с использованием дополнительных
параметров: вывод
Say we have this string: Acme Inc subnet 10.11.12.13/24
When the code runs we get:
Customer name:
Subnet:
IPA
CIDR mask:
FW Object:

Acme Inc
10.11.12.13/24
10.11.12.13
24
acme_inc_subnet_10.11.12.13-24

Условные подстановки
Некоторые из подстановок, приведенных в примере 4.1, являются
условными, то есть выполняются только при определенных условиях.
Того же эффекта можно добиться, используя операторы if, но идиомы
позволяют сократить код в некоторых распространенных случаях.
Особенность условных подстановок — двоеточие, за которым следует
другой специальный символ: минус (-), плюс (+) или знак равенства (=).
Они проверяют, была ли создана переменная и имеет ли она значение.
Во втором случае значением переменной является пустая строка. Несозданной (неустановленной) считается переменная, которой еще не
было присвоено значение или которая была явно удалена с помощью
команды unset. Позиционные параметры ($1, $2 и т. д.) считаются несозданными, если пользователь не передал параметр в соответствующей позиции.
Если в условные подстановки не включить двоеточие, то они выполняются, только если переменная не создана; для созданных переменных возвращаются их фактические значения, даже если это пустая
строка.

Значения по умолчанию
Распространенным случаем использования значений по умолчанию
является сценарий с одним необязательным параметром. Если параметр

Условные подстановки  

67

не указан при вызове сценария, следует использовать значение по умолчанию. Например, в bash можно написать такой код:
LEN=${1:-5}

Он присвоит переменной LEN значение первого параметра ($1), если он
был указан, или значение 5. Вот пример сценария:
LEN="${1:-5}"
cut -d',' -f2-3 /tmp/megaraid.out | sort | uniq -c | sort -rn | head -n "$LEN"

Он извлекает второе и третье поля из записей в CSV-файле /tmp/
megaraid.out, сортирует, подсчитывает количество вхождений каждой
пары значений, а затем выводит первые пять пар из списка. Значение
по умолчанию 5 можно переопределить и отобразить, например, первые
три, 10 или сколько пожелаете пар, просто указав нужное количество
в единственном параметре сценария.

Списки значений, разделенных запятыми
Другая разновидность условной подстановки с использованием знака +
проверяет, присвоено ли переменной какое-то (непустое) значение,
и, если присвоено, возвращает заданное значение. Звучит странно:
если переменная имеет значение, то зачем возвращать какое-то другое
значение?
Однако эта, казалось бы, странная логика имеет полезное применение:
создание списка значений, разделенных запятыми. Обычно такой список
создается многократным добавлением значений. При этом возникает
необходимость в операторе if, чтобы не добавить лишнюю запятую
в начале или в конце списка, но этого не требуется при использовании
идиомы соединения:
for fn in * ; do
S=${LIST:+,}
# S -- разделитель
LIST="${LIST}${S}${fn}"
done

Также ознакомьтесь с примером 7.1.

68  Глава 4. Язык переменных

Изменение значения
Ни одна из описанных выше подстановок не изменяет значение самой переменной. Однако есть исключение. Выражение ${VAR:=value},
действует так же, как предыдущая идиома значения по умолчанию,
но с одним важным исключением. Если переменная VAR имеет пустое
значение или не создана, то ей будет присвоено значение value (на что
намекает знак равенства) и оно же будет возвращено (если VAR уже имеет
некоторое непустое значение, то выражение возвратит его). Но такой
способ присваивания значения не работает с позиционными параметрами, такими как $1, поэтому он используется довольно редко.

$RANDOM
В bash есть очень удобная переменная $RANDOM. В справочном руководстве
по bash1 указано:
При каждом обращении к этой переменной генерируется случайное целое число от 0 до 32 767. Присвоение значения этой переменной запускает генератор случайных чисел.
Эта переменная не годится для криптографических функций, но вполне
подойдет для моделирования игрового кубика или добавления шума
в слишком предсказуемые операции. Мы используем ее в разделе «Простой пример подсчета слов» в главе 7.
Пример 4.3 показывает, как с помощью $RANDOM выбрать случайный
элемент из списка.
Пример 4.3. Выбор случайного элемента из списка
declare -a mylist
mylist=(foo bar baz one two "three four")
range=${#mylist[@]}
random=$(( $RANDOM % $range )) # от 0 до числа длины списка

1

https://oreil.ly/aQSXr.

Подстановка команд  

69

echo "range = $range, random = $random, choice = ${mylist[$random]}"
# Более короткий способ, но его будет трудно понять через полгода после написания:
# echo "choice = ${mylist[$(( $RANDOM % ${#mylist[@]} ))]}"

Иногда можно увидеть и такое применение $RANDOM:
TEMP_DIR="$TMP/myscript.$RANDOM"
[ -d "$TEMP_DIR" ] || mkdir "$TEMP_DIR"

Однако это решение чревато состоянием гонки и, очевидно, является
шаблоном. Кроме того, иногда важно иметь представление о том, что
захламляет $TMP. Не забудьте поставить ловушку trap (см. раздел «Это
ловушка!» в главе 9), чтобы прибрать за собой. Мы рекомендуем подумать об использовании mktemp, хотя обсуждение этой проблемы выходит
за рамки идиом bash.
$RANDOM и dash
Переменная $RANDOM недоступна в dash — интерпретаторе командной оболочки, на который указывает ссылка /bin/sh в некоторых дистрибутивах
Linux. Актуальные версии Debian и Ubuntu используют dash, так как он
меньше по объему и быстрее, чем bash, что помогает им быстрее загружаться. Но некоторые возможности, доступные в bash, в них работать не
будут. Однако в Zsh эта переменная имеется.

Подстановка команд
Мы уже использовали подстановку команд в главе 2, но не говорили об
этом. Старый способ такой подстановки в оболочке Bourne заключался в использовании обратных кавычек (обратных апострофов) ``. Мы
рекомендуем использовать современный более читаемый и POSIXсовместимый синтаксис $(). Вы можете встретить обе формы, потому
что именно так вывод команд переносится в значения переменных:
unique_lines_in_file="$(sort -u "$my_file" | wc -l)"

70  Глава 4. Язык переменных

Две следующих строки делают то же самое, но вторая использует внутренние механизмы командной оболочки и потому работает быстрее:
for arg in $(cat /some/file)
for arg in $(< /some/file)
# Быстрее, чем с вызовом команды cat

Подстановка команд
Подстановка команд имеет решающее значение в сфере DevOps, потому
что позволяет собирать и использовать данные, существующие только во
время выполнения кода. Например:
instance_id=$(aws ec2 run-instances --image $base_ami_id ... \
--output text --query 'Instances[*].InstanceId')
state=$(aws ec2 describe-instances --instance-ids $instance_id \
--output text --query 'Reservations[*].Instances[*].State.Name')

Вложенная подстановка команд
Использование `` при вложенной подстановке команд выглядит неряшливо и чревато ошибками из-за сложного синтаксиса. Значительно проще
использовать $(), как показано ниже:
### Просто работает
$ echo $(echo $(echo $(echo inside)))
inside
### Ошибка
$ echo `echo `echo `echo inside```
echo inside
### Работает, но выглядит ужасно
$ echo `echo \`echo \\\`echo inside\\\`\``
inside

Спасибо нашему рецензенту Яну Миеллу (Ian Miell), предоставившему
код для примера.

В заключение: стиль и удобочитаемость  

71

В заключение: стиль и удобочитаемость
Ссылаясь на переменную в bash, можно изменять извлекаемое или присваиваемое значение. Несколько специальных символов в конце ссылки
на переменную могут удалять символы в начале или в конце строкового
значения, менять их регистр, замещать символы или возвращать подстроку. Востребованность и удобство этих возможностей породили
идиомы для значений по умолчанию, замены команд basename и dirname,
а также создание списков значений, разделенных запятыми, без явного
использования оператора if.
Подстановка переменных — замечательная особенность bash, и мы советуем использовать ее. Однако мы также настоятельно рекомендуем
снабжать код комментариями, чтобы было ясно, какого рода подстановку
вы выполняете. Тот, кому впоследствии придется разбирать ваш код,
будет вам за это благодарен.

ГЛАВА 5

Выражения и арифметика

Командная оболочка bash, с одной стороны, позволяет сделать многие
операции несколькими способами, с другой стороны, почти одинаковый синтаксис может приводить к совершенно разным действиям.
Часто разница заключается лишь в нескольких специальных символах.
Мы уже видели выражения ${VAR} и ${#VAR}, первое из которых возвращает значение переменной, второе — длину этого значения (см. раздел
«Ссылка на переменную» в главе 4). Или ${VAR[@]} и ${VAR[*]}, отличающиеся поддержкой кавычек (см. раздел «Кавычки и пробелы»
в главе 2).
Многие идиомы bash вызывают вопросы: следует ли использовать двойные или одинарные квадратные скобки и не лучше ли вообще отказаться от их применения, или в чем разница между (( ... )) и $(( ... ))?
Обычно разные варианты использования символов имеют что-то общее,
намекающее на сходство причин, лежащих в основе синтаксиса. Но в некоторых случаях синтаксис обусловлен исключительно традициями.
Давайте рассмотрим и попробуем объяснить некоторые из идиоматических шаблонов и арифметических выражений bash.
Только целочисленные вычисления
Командная оболочка bash поддерживает только целочисленную арифметику, так как ее задачи в большинстве случаев связаны с подсчетом чего-либо:
итераций, количества файлов, размеров в байтах и т. п. Но как быть, если
потребуется выполнить вычисления с плавающей точкой? В конце концов,

Арифметика  73

современная версия команды sleep может принимать дробные значения.
Например, sleep 0.25 приостановит работу на четверть секунды. А если
потребуется пауза на периоды, кратные четверти секунды? Вы могли бы
написать sleep $(( 6 * 0.25 )), но этот прием не сработает.
Самое простое решение — выполнить вычисления с помощью другой
программы, такой как bc или awk. Ниже приведен сценарий fp, который
можно поместить в каталог ~/bin или другой, находящийся в списке
в переменной PATH (не забудьте дать ему права на выполнение):
# /bin/bash # fp -- реализует операции с плавающей точкой через awk
# порядок использования: fp "выражение"
awk "BEGIN { print $* }"

При таком сценарии команда sleep $(fp "6 * 0.25") выполнит желаемые вычисления с плавающей точкой. Конечно, вычисления производит
не сам интерпретатор bash, но он помогает выполнять расчеты.

Арифметика
Язык bash в основном ориентирован на операции со строками, однако,
встретив в сценарии двойные круглые скобки, знайте, что здесь выполняются арифметические вычисления — с целыми числами, а не со
строками. По варианту цикла for с двойными круглыми скобками вам
знакома следующая конструкция:
for ((i=0; i x * 4 )) ; then
# Сделать что-то
fi

Почему на этот раз двойные круглые скобки используются без предшествующего им знака $?
В первом случае в операции присваивания нам нужно получить значение
выражения. Поэтому, как и в случае с переменными, мы добавляем $, подсказывающий интерпретатору, что нам нужно значение. Но в операторе
if знак доллара не нужен, так как для принятия решения достаточно
логического значения истина/ложь. Если при вычислении выражения
внутри двойных скобок (без знака $) получается ненулевой результат,
то возвращается статус 0, что в bash считается «истинным» значением.
Иначе возвращается статус 1, что в bash означает «ложь».
Обратили внимание, что мы говорим «возвращается статус»? Это связано с тем, что двойные круглые скобки без знака $ интерпретируются
как выполнение одной или нескольких команд. Они не возвращают
результат вычислений, который можно было бы присвоить переменной.
Однако в некоторых случаях их можно использовать, чтобы присвоить
некоторой переменной новое значение, потому что bash поддерживает
некоторые операторы присваивания в стиле языка C. Вот пример трех
полных операторов bash:
(( step++ ))
(( median_loc = len / 2 ))
(( dist *= 4 ))

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

Арифметика  75

Можно ли записать операторы из предыдущего примера, используя синтаксис со знаком доллара и двойными круглыми скобками? Да, причем
такой вариант выглядит более привычным:
step=$(( step + 1 ))
median_loc=$(( len / 2 ))
dist=$(( dist * 4 ))

Не следует использовать выражение $((step++)) как самостоятельную
инструкцию в отдельной строке, потому что оно вернет числовое значение, которое интерпретатор воспримет как имя выполняемой команды.
Если step++ даст результат 3, то оболочка попытается отыскать команду
с именем 3.
Не забывайте о пробелах
В операции присваивания значения переменной пробелы вокруг знака
равенства не допускаются. Синтаксически вся инструкция присваивания
должна быть одним «словом». Однако внутри круглых скобок допускается
использовать пробелы, потому что круглые скобки определяют границы
этого «слова».

Теперь рассмотрим способ вычисления арифметических выражений,
сохранившийся из прошлого. Чтобы получить эффект двойных круглых скобок без знака $, можно использовать встроенную команду let.
Взгляните на следующие пары эквивалентных инструкций:
(( step++ )) # То же, что и:
let "step++"
(( median_loc = len / 2 )) # То же, что и:
let "median_loc = len / 2"
(( dist *= 4 )) # То же, что и:
let "dist*=4"

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

76  Глава 5. Выражения и арифметика

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

Круглые скобки не нужны
Хотя bash в основном ориентирован на операции со строками, возможны исключения. Переменную можно объявить как целочисленную,
например: declare -i MYVAR. Затем с ней можно выполнять арифметические операции и присваивать ей значение без использования двойных
круглых скобок или знака $. Эти возможности демонстрирует сценарий
seesaw.sh:
declare -i SEE
X=9
Y=3
SEE=X+Y
# Здесь будет выполнена арифметическая операция
SAW=X+Y
# Интерпретируется как строковый литерал
SUM=$X+$Y
# Интерпретируется как конкатенация строк
echo "SEE = $SEE"
echo "SAW = $SAW"
echo "SUM = $SUM"

Результат выполнения этого сценария наглядно демонстрирует ориентацию bash в основном на операции со строками. Значения SAW и SUM
формируются строковыми операциями. Только SEE получает числовое
значение — результат арифметического действия:
$ bash seesaw.sh
SEE = 12
SAW = X+Y
SUM = 9+3
$

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

Составные команды  

77

Составные команды
Разумеется, вам хорошо знакома практика записывать одиночные команды в отдельных строках сценария. В условном выражении оператор if
может оценивать успешность выполнения этой команды и инициировать
некоторые действия в зависимости от результата. В главе 1 мы представили идиому оператора if «проверка условия без if». Теперь давайте
взглянем на простой оператор if с единственной командой:
if cd $DIR ; then # Выполнить что-то...

Сравните его со следующими выражениями:
if [ $DIR ]; then # Выполнить что-то...
if [[ $DIR ]]; then # Выполнить что-то...

Почему в двух последних примерах используются квадратные скобки,
а в первом нет, и есть ли разница? Почему во втором примере используются одинарные квадратные скобки, а в третьем — двойные?
Без квадратных скобок оболочка выполнит команду (в нашем примере
cd), которая вернет признак успешного или неуспешного выполнения.
Этот признак интерпретируется оператором if как истинное или ложное
значение и используется для выбора между операторами then или else
(если он есть). Но bash позволяет поместить в оператор if и целый конвейер команд (например, cmd | sort | wc ). Статус выполнения последней
команды в конвейере определяет значение условного выражения — истинное или ложное (такой подход может вызвать ошибки, которые очень
трудно найти; см. описание set -o pipefail в разделе «Неофициальный
строгий режим в bash» в главе 9).
Синтаксис с одинарными квадратными скобками запускает встроенную
команду test. Открывающая квадратная скобка — это встроенная команда оболочки, аналог команды test, но отличающаяся обязательным
конечным аргументом ]. Двойные квадратные скобки технически являются ключевым словом, определяющим составную команду. По своим

78  Глава 5. Выражения и арифметика

свойствам это ключевое слово очень похоже на одинарные квадратные
скобки и команду test, но имеет и некоторые отличия.
Синтаксис с одинарными или двойными квадратными скобками используется для выполнения некоторой логики и сравнений, то есть
условных выражений, проверяющих состояние, например присутствие
файла, наличие у него определенных разрешений, или имеет ли переменная непустое значение. Полный список проверок, которые можно
выполнить в bash, вы найдете в странице справочного руководства man
bash в разделе Conditional Expressions1, а для быстрой справки используйте команду help test.
Наш предыдущий пример проверяет, имеет ли переменная DIR непустое
значение (ненулевую длину). Ту же проверку можно записать иначе:
if [[ -n "$DIR" ]]; then ...

Можно проверить и наоборот, имеет ли переменная пустое значение
(с нулевой длиной) или вообще не была установлена:
if [[ -z "$DIR" ]]; then ...

Так чем же отличаются проверки с одинарными и двойными квадратными скобками? Таких отличий несколько, но все они незначительны.
Вероятно, самое важное отличие заключается в том, что синтаксис
с двойными квадратными скобками поддерживает дополнительный оператор сравнения =~, позволяющий использовать регулярные выражения:
if [[ "$FILE_NAME" =~ .*xyzz*.*jpg ]]; then ...

Регулярные выражения
Рассматриваемый случай — единственный, когда в bash могут использоваться регулярные выражения! И помните: не заключайте их в кавычки,
иначе сопоставление будет выполняться буквально, а не как принято при
их применении.
1

https://oreil.ly/Bn5gv (перевод страницы на русский язык можно найти по адресу
https://www.opennet.ru/man.shtml?topic=bash&category=1. — Примеч. пер.).

Составные команды  

79

Еще одно отличие между одинарными и двойными квадратными скобками носит больше стилистический характер, но влияет на переносимость.
Следующие две формы делают одно и то же:
if [[ $VAR == "literal" ]]; then ...
if [ $VAR = "literal" ]; then ...

Программистам на C и Java использование одного знака равенства
для сравнения может показаться ошибкой, но в условных выражениях
bash = и == означают одно и то же. Одиночный знак равенства предпочтительнее в синтаксисе с одинарными квадратными скобками, так как
соответствует стандарту POSIX (по крайней мере так утверждается на
странице справочного руководства man bash).
Тонкое отличие
Внутри двойных квадратных скобок операторы < и > выполняют «лексикографическое сравнение с использованием текущих региональных
настроек», тогда как test и [ выполняют простое сравнение на основе
ASCII.
Также в одинарных квадратных скобках < и > необходимо экранировать
(например, if [ $x \> $y]), иначе они будут интерпретироваться как операторы перенаправления. Почему? Дело в том, что одиночная квадратная
скобка, как и test, является встроенной командой, а не ключевым словом,
а ввод/вывод команд можно перенаправлять. Двойные квадратные скобки,
напротив, являются ключевым словом, поэтому bash не рассматривает их
как перенаправление. По этой причине из двух синтаксических форм мы
предпочитаем синтаксис с двойными скобками.

Выражения с одинарными и двойными квадратными скобками позволяют использовать старый синтаксис числовых сравнений, напоминающий
Fortran. Например, -le — это оператор «меньше или равно» (less-than-orequal-to). Но здесь кроется еще одно отличие в применении квадратных
скобок. Аргументы по обе стороны от этого оператора в одинарных квадратных скобках должны быть простыми целыми числами. В двойных
квадратных скобках операнды могут быть целыми арифметическими

80  Глава 5. Выражения и арифметика

выражениями, хотя и без пробелов, если они не заключены в кавычки.
Например:
if [[ $OTHERVAL*10 -le $VAL/5 ]] ; then ...

Однако для сравнения арифметических выражений лучше подходит
синтаксис с двойными круглыми скобками. Он позволяет использовать
более привычные операторы сравнения в стиле C/Java/Python и дает
больше свободы в отношении расстановки пробелов:
if (( OTHERVAL * 10 &2

Usage_Message {
"usage: $0 value pathname"
"where value must be positive"
"and pathname must be an existing file"
"for example: $0 25 /tmp/scratch.csv"

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

88  Глава 6. Функции

в STDERR, но перенаправление не обязательно помещать в каждую строку
в функции — достаточно одного указания за закрывающей фигурной
скобкой. Это не только сэкономит время, но и избавляет от необходимости помнить о добавлении переадресации к новым строкам при
изменении сценария.
Обратите внимание, что для простоты и ясности мы нарушили некоторые из наших рекомендаций по стилю оформления функций; подробности см. в разделе «Функции» в главе 11.

Функция printf
Знакомым с функцией printf в таких языках, как C и Java, материал
этого раздела будет освоить несколько проще. Мы решили рассмотреть
printf в этой главе, хотя она является встроенной функцией.
Функция форматированного вывода printf (print formatted) редко используется в сценариях bash, потому что обычно ей предпочитают команду echo. Однако встроенная функция имеет несколько идиоматических
расширений, которые вы должны знать и уметь применять. Кроме того,
она определена встандарте POSIX, в отличие от встроенной в bash версии echo, и потому лучше переносима и более пригодна для сценариев,
которые должны работать в системах, отличных от Linux.
Мы рассмотрим только встроенную версию printf, хотя у многих из
вас наверняка имеется в системе внешний двоичный файл, который не
связан с bash и использует другие параметры:
$ type -a printf
printf is a shell builtin
printf is /usr/bin/printf

Встроенная функция printf будет использоваться всегда, если не указать
полный путь (/usr/bin/printf) или префикс env. Сравните printf --help
и env printf --help, чтобы увидеть различия.

Функция printf  

89

Мы не будем вдаваться в длинный список стандартных спецификаторов
формата printf; они описаны во многих других источниках.
Получить дополнительную информацию о версии printf можно с помощью следующих команд:
help printf или printf --help;
man 1 printf;

если имеется двоичный файл: /usr/bin/printf --help или env
printf --help.

Вывод POSIX
Если ваш сценарий должен работать в операционных системах, отличных от Linux, то предпочтительно использовать printf вместо echo. Это
просто, но, в отличие от echo, функция printf не добавляет по умолчанию
перевод строки:
printf '%s\n' # Перевод строки НЕ добавляется по умолчанию
printf '%b\n' # С переводом строки. Расширьте свои знания управляющих
# последовательностей

Спецификаторы формата printf можно заключить как в одинарные,
так и в двойные кавычки, следуя обычным правилам интерполяции
переменных (в одинарных кавычках переменные интерполироваться не
будут). Однако управляющие последовательности, такие как \n, будут
интерпретироваться и в одинарных, и в двойных кавычках. Как уже
отмечалось, мы предпочитаем одинарные кавычки, исключая интерполяцию. Хотя иногда она требуется, помогая понять, что мы делаем
что-то неправильно.
Дополнительную информацию по этой теме можно найти по ссылкам:
• https://unix.stackexchange.com/a/65819;
• https://www.in-ulm.de/~mascheck/various/echo+printf.

90  Глава 6. Функции

Получение и использование даты и времени
В bash 4.2 была добавлена поддержка спецификатора формата printf
но по умолчанию выводилась дата начала эпохи Unix
(1970-01-01 00:00:00 -0000). В bash 4.3 значение по умолчанию изменилось на более полезное — текущие дата и время. Спецификатор имеет
два специальных аргумента: "-1", означающий «текущие дата и время»,
и "-2", означающий «дата и время вызова оболочки». Также вы можете
задать время в секундах от начала эпохи с последующим преобразованием его в удобочитаемое представление. Но если вы имеете в виду
«сейчас», то мы рекомендуем использовать аргумент "-1" для согласованности и ясности намерений:
%(формат_даты)T,

### Установить переменную $today с помощью -v
$ printf -v today '%(%F)T' '-1'
$ echo $today
2021-08-13
### Простое журналирование (обе инструкции выводят одинаковое время)
$ printf '%(%Y-%m-%d %H:%M:%S %z)T: %s\n' '-1' 'Your log message here'
$ printf '%(%F %T %z)T: %s\n' '-1' 'Your log message here'
2021-08-13 12:48:33 -0400: Your log message here
### Какой дате и времени соответствует число секунд 1628873101?
$ printf '%(%F %T)T = %s\n' '1628873101' 'Epoch 1628873101 to human readable'
2021-08-13 12:45:01 = Epoch 1628873101 to human readable

Форматированный вывод в переменную с помощью printf
В справке по printf вы могли обратить внимание на параметр -v var,
позволяющий сохранить вывод в переменной вместо вывода на экран,
подобно функции sprintf в C. В свое время мы рассмотрим такой способ
применения printf.

Дополнительные сведения о выводе даты и времени см. в разделе «Журналирование в bash» в главе 10.
Мы предпочитаем использовать встроенную версию printf, но GNUверсия1 date работает лучше, чем printf %(формат_даты)T , позволяя
1

GNU (https://oreil.ly/eG6nV) — это рекурсивная аббревиатура, расшифровывающаяся
как «GNU’s Not Unix» (GNU — не Unix).

В заключение: стиль и удобочитаемость  

91

выполнять арифметические действия с датами, например date -d '2 months
ago' '+%B' сообщит название позапрошлого месяца:
$ date -d '2 months ago' '+%B'
August

Устаревший bash в Mac
Обратите внимание, что для printf %(формат_даты)T требуется версия
bash не ниже 4.2, а лучше — 4.3 или выше. В старой версии bash 3.2, которая устанавливается по умолчанию в Mac, этот спецификатор работать
не будет. См. раздел «bash на Mac» во вступлении.

printf для повторного использования или отладки
В справке bash говорится: «%q экранирует символы в аргументе, так
чтобы его можно было повторно использовать в качестве ввода», — и мы
используем эту особенность в главе 7, чтобы показать экранирование
строк, но для нетерпеливых приведем простой пример:
$ printf '%q' "This example is from $today\n"
This\ example\ is\ from\ 2021-08-13\\n

Эта особенность может пригодиться для повторного использования
вывода в другом месте, создания форматированного вывода (см. также
«Списки значений, разделенных запятыми» в главе 4) и отладки, когда
требуется видеть скрытые управляющие символы и поля (см. также
раздел «Отладка в bash» в главе 10).

В заключение: стиль и удобочитаемость
Функции в bash очень похожи на внутренние сценарии. Их вызов напоминает вызов команды, а их аргументы доступны, подобно параметрам
сценария (как $1, $2 и т. д.). Функции следует помещать в начало сценария, чтобы их определения располагались выше вызовов. Как и в любом

92  Глава 6. Функции

другом языке, функции должны быть короткими и ориентированными
на решение одной задачи. Использование одного перенаправления
ввода/вывода для всей функции поможет избавиться от необходимости
перенаправлять вывод в каждой инструкции в теле функции.
Самая большая опасность, заключающаяся в функциях, — ссылки на
переменные. Функции могут возвращать значения через переменные
или путем вывода в STDOUT. В последнем случае вызывающий код сможет перенаправить вывод функции следующей команде. Для возврата
значений из функций можно использовать глобальные переменные, но
тогда есть риск изменить переменные, которые не должны изменяться.
В таком случае может помочь использование объявления local, но не
забудьте про динамическую область видимости — локальные переменные могут быть доступны другим функциям, вызываемым из основной.
Обязательно описывайте такие вещи в комментариях.
Встроенная функция printf во многом похожа на одноименную функцию
в других языках. Помимо стандартных спецификаторов формата, printf
в bash имеет несколько полезных идиоматических расширений. Нам
особенно нравится отсутствие необходимости создавать подоболочку
для запуска команды date. Это удобно, если требуется только зафиксировать время, например при журналировании событий.

ГЛАВА 7

Списки и хеши

Компьютеры хорошо вычисляют и систематизируют данные. Мы можем использовать сценарии для вычислений и организации структур
данных, а строительными блоками для них могут служить массивы.
Массивы с самого начала поддерживались в bash, причем в версии
4.0 появилась поддержка ассоциативных массивов. Код, связанный
с массивами, нередко трудно читать, отчасти потому что bash имеет
богатую историю и важно сохранять обратную совместимость версий,
но также и по вине некоторых разработчиков, склонных все усложнять.
На самом деле в массивах нет ничего сложного, и в bash с ними вполне
можно работать.
Напомним, что в информатике и программировании массивы — это
переменные, содержащие несколько элементов, на которые можно
ссылаться с использованием целочисленных индексов. Иначе говоря,
массив — это переменная, содержащая список, а не скаляр или одиночное значение. Ассоциативный массив — это список, элементы которого
индексируются строками, а не целыми числами. То есть это список пар
«ключ – значение», образующий словарь или таблицу поиска, в котором
ключи хешируются для формирования области памяти.
В документации bash используются термины массив и ассоциативный массив, но вы можете называть их списками и хешами или даже
словарями, если вам так удобнее. Также в документации bash используется понятие нижний индекс, под которым можно понимать просто индекс. Обычно мы стараемся следовать документации bash для

94  Глава 7. Списки и хеши

согласованности, но в данном случае будем использовать более распространенные и понятные термины: список, хеш и индекс.
Хотя списки (массивы) появились раньше, хеши (ассоциативные массивы) немного проще в обращении. В хешах никогда не возникает вопроса
о том, указывать ли индексы (нижние индексы) при ссылке на элементы,
потому такое указание является обязательным. Целочисленные индексы
списка могут только подразумеваться, причем некоторые операции над
ними не имеют смысла для хешей.
В руководстве указано, что «bash поддерживает переменные, способные
хранить одномерные массивы с целочисленными индексами и ассоциативные массивы переменных». В действительности в bash можно
создавать многомерные структуры, но они будут выглядеть уродливо,
и, скорее всего, эта затея закончится плачевно. Если вам действительно понадобятся такие структуры, лучше реализуйте свою задумку на
другом языке.
Не все версии bash поддерживают хеши
Разбирая материалы этой главы, обратите внимание на вашу версию bash.
Как отмечено выше, поддержка хешей (ассоциативных массивов) появилась только в версии 4.0, и потребовалось еще несколько релизов, чтобы
отшлифовать некоторые детали. Например, только в версии 4.3 появилась
возможность использовать $list[-1] для ссылки на последний элемент
массива вместо жутковатой конструкции $mylist[${#mylist[*]}-1]
(где ${#mylist[*]} — количество элементов).
Как мы уже говорили в разделе «bash на Mac» во вступлении, не забывайте,
что по умолчанию в Mac устанавливается очень старая версия bash. Новые
версии можно найти на MacPorts, Homebrew или Fink.

Не POSIX
Следует особо отметить, что массивы (как списки, так и хеши) не стандартизированы в POSIX. Поэтому, если вас беспокоит переносимость кода за
пределы bash, проявляйте особую осторожность при их использовании.
Например, синтаксис Zsh немного отличается, поэтому представленные
далее примеры не будут работать на Mac с этой командной оболочкой.

Сходные черты  

95

Сходные черты
Списки и хеши в bash очень похожи, поэтому мы начнем с обзора общих
черт, а затем перейдем к отличиям. На самом деле списки можно рассматривать как разновидность хешей, которые просто имеют упорядоченные
целочисленные индексы. Конечно, использовать такую интерпретацию
необязательно, но вполне возможно.
Списки по своей природе — упорядоченные коллекции, тогда как хеши —
нет, а такие операции, как сдвиг (shift) или добавление в конец (push),
имеют смысл только для упорядоченных наборов элементов. С другой
стороны, вам никогда не понадобится сортировать ключи в списке, но
эта операция имеет определенный смысл для хешей.
Случайное присваивание
Присваивание без указания нижнего индекса изменит нулевой элемент,
поэтому myarray=foo приведет к созданию или изменению $myarray[0],
даже если это хеш!

В документации bash указано следующее:
Если используется индекс @ или *, ссылка на массив возвращает все
его элементы. Эти индексы различаются, только когда ссылка заключается в двойные кавычки. Если "${name[*]}" разворачивается
в одну строку, включающую значения всех элементов массива, разделенные первым символом из переменной IFS (см. раздел «Применение $IFS ради забавы и практической выгоды при чтении
файлов» в главе 9), то "${name[@]}" разворачивается в коллекцию
строк, содержащих отдельные элементы массива.
Неочевидные правила, не так ли? Мы уже говорили в разделе «Кавычки
и пробелы» в главе 2, что ошибки в этих нюансах могут привести к серьезным неприятностям. В примере 7.1 мы используем printf "%q" с вертикальной чертой (|), чтобы разделить части строк (отдельные «слова»)
при выводе результатов выполнения кода. Правила экранирования — те
же, что были описаны в главе 2, только теперь они применяются в контексте списка или хеша.

96  Глава 7. Списки и хеши

Списки
Как мы уже говорили, массивы, также известные как списки, — это переменные, содержащие несколько элементов, которые индексируются
целыми числами.
В bash индексация начинается с нуля, а массивы могут объявляться
как с помощью команд declare -a, local -a, readonly -a, так и простым
присваиванием, например: mylist[0]=foo или mylist=() (пустой список).
После объявления переменной списком простое присваивание, такое
как mylist+=(bar), будет действовать как операция добавления элемента
в конец списка. Но обращайте внимание на знаки + и (), оба компонента
играют важную роль. В табл. 7.1 представлен пример списка.
Таблица 7.1. Пример списка в bash
Элемент

Значение

mylist[0]

foo

mylist[1]

bar

mylist[2]

baz

Типичные операции со списками:
объявление переменной списком;
присваивание списку одного или нескольких значений;
если список используется как стек (представьте очередь в столовой: FIFO — first in, first out, то есть первым пришел, первым вышел):
• добавление в конец (push);
• извлечение из начала (pop);
отображение (вывод) всех значений для целей отладки или повторного использования;
ссылка на одно или на все значения (for или for each);

Списки  97

ссылка на подмножество (срез) значений;
удаление одного или нескольких значений;
удаление всего списка.
Давайте не будем рассуждать обо всех этих операциях, а просто рассмотрим их на примере, чтобы вы могли выбрать нужные идиомы, когда
они вам понадобятся (пример 7.1).
Пример 7.1. Примеры операций со списками в bash: код
#!/usr/bin/env bash
# lists.sh: примеры операций со списками в bash
# Автор и дата: _bash Idioms_ 2022
# Имя файла в bash Idioms: examples/ch07/lists.sh
#_________________________________________________________________________
# Не работает в Zsh 5.4.2!
# Книжные страницы не так широки, как экран компьютера!
FORMAT='fmt --width 70 --split-only'
# Объявление списка ❶
# declare -a mylist # Можно использовать такую конструкцию, или `local -a`,
# или `readonly -a`, или:
mylist[0]='foo'
# Объявляет переменную списком и присваивает значение
# элементу mylist[0]
# Можно также одновременно объявить переменную списком
# и присвоить значения его элементам:
#mylist=(foo bar baz three four "five by five" six)
# Добавление элемента в список, обратите внимание на += и () ❷
###mylist=(bar)
# Изменит значение mylist[0]
mylist+=(bar)
# mylist[1]
mylist+=(baz)
# mylist[2]
mylist+=(three four)
# mylist[3] И mylist[4]
mylist+=("five by five")
# mylist[5] Обратите внимание на пробелы и кавычки
mylist+=("six")
# mylist[6]
# Обратите внимание на знак "+": мы предполагаем,
# что нулевому элементу уже присвоено значение foo
#mylist+=(bar baz three four "five by five" six)
# Вывод содержимого списка ❸
echo -e "\nThe element count is: ${#mylist[@]} or ${#mylist[*]}"
echo -e "\nThe length of element [4] is: ${#mylist[4]}"

98  Глава 7. Списки и хеши

echo -e "\nDump or list:"
declare -p mylist | $FORMAT
echo -n
"\${mylist[@]}
echo -en
"\n\${mylist[*]}
echo -en "\n\"\${mylist[@]}\"
echo -en "\n\"\${mylist[*]}\"

=
=
=
=

"
"
"
"

;
;
;
;

printf
printf
printf
printf

"%q|" ${mylist[@]}
"%q|" ${mylist[*]}
"%q|" "${mylist[@]}"
"%q|" "${mylist[*]}"

echo -e " # Broken!" # Предыдущая строка кода -- ошибочная
# и не выводит символа перевода строки
# См. `help printf` или раздел "printf для повторного использования или отладки"
# в главе 6. Мы использовали этот код, чтобы показать отдельные слова:
# %q экранирует аргумент так, что его можно повторно использовать
# в качестве входных данных в командной оболочке
# "Объединение" значений ❹
function Join { local IFS="$1"; shift; echo "$*"; } # Односимвольный разделитель!
# Обратите внимание, что Join выше использует "$*", а не "$@"!
echo -en "\nJoin ',' \${mylist[@]} = "; Join ',' "${mylist[@]}"
function String_Join {
local delimiter="$1"
local first_element="$2"
shift 2
printf '%s' "$first_element" "${@/#/$delimiter}"
# Выводит первый элемент, затем повторно использует формат '%s'
# для отображения остальных элементов (из аргументов функции $@),
# но добавляет префикс $delimiter, "замещая" пустой начальный шаблон (/#)
# значением $delimiter
}
echo -n "String_Join '' \${mylist[@]} = " ; String_Join '' "${mylist[@]}"
# Обход значений ❺
echo -e "\nforeach \"\${!mylist[@]}\":"
for element in "${!mylist[@]}"; do
echo -e "\tElement: $element; value: ${mylist[$element]}"
done
echo -e "\nBut don't do this: \${mylist[*]}"
for element in ${mylist[*]}; do
echo -e "\tElement: $element; value: ${mylist[$element]}"
done
# Операции со срезами (подмножествами) списка, сдвиг и выталкивание
echo -e "\nStart from element 5 and show a slice of 2 elements:"
printf "%q|" "${mylist[@]:5:2}"
echo '' # Предыдущая инструкция не выводит символ перевода строки



echo -e "\nShift FIRST element [0] (dumped before and after):"
declare -p mylist | $FORMAT
# Отображается до
mylist=("${mylist[@]:1}")
# Срез, начинающийся с первого элемента,
# кавычки обязательны

Списки  99

#mylist=("${mylist[@]:$count}")
declare -p mylist | $FORMAT

# Срез, начинающийся с элемента #count
# Отображается после

echo -e "\nPop LAST element (dumped before and after):"
declare -p mylist | $FORMAT
unset -v 'mylist[-1]'
# В bash версий 4.3 и выше
#unset -v "mylist[${#mylist[*]}-1]" # В более старых версиях
declare -p mylist
# Удаление срезов ❼
echo -e "\nDelete element 2 using unset (dumped before and after):"
declare -p mylist
unset -v 'mylist[2]'
declare -p mylist
# Удаление всего списка
unset -v mylist



Необходимые пояснения:

❶ Объявление переменной массивом. Здесь мы называем переменную

«массивом», а не «списком», потому что используем ключ -a (array —
массив).

❷ Присваивание списку одного или нескольких значений.
❸ Вывод всех значений для отладки или повторного использования; см.

пример 4.1.

❹ Две разные функции объединения; см. также раздел «Списки значений, разделенных запятыми» в главе 4.

❺ Обход значений в списке; см. пример 4.1.
❻ Операции со срезами (подмножествами) списка, сдвиг и выталкивание.
❼ Удаление срезов.
❽ Удаление всего списка. Используйте unset с осторожностью, потому

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

100  Глава 7. Списки и хеши

лучше заключить переменную в кавычки. Еще безопаснее использовать
ключ -v, чтобы заставить unset рассматривать аргумент как переменную,
например unset -v 'list'.
В примере 7.2 показан вывод сценария из примера 7.1.
Пример 7.2. Примеры операций со списками в bash: вывод
The element count is: 7 or 7
The length of element [4] is: 4
Dump or list:
declare -a mylist=([0]="foo" [1]="bar" [2]="baz" [3]="three"
[4]="four" [5]="five by five" [6]="six")
${mylist[@]} = foo|bar|baz|three|four|five|by|five|six|
${mylist[*]} = foo|bar|baz|three|four|five|by|five|six|
"${mylist[@]}" = foo|bar|baz|three|four|five\ by\ five|six|
"${mylist[*]}" = foo\ bar\ baz\ three\ four\ five\ by\ five\ six| # Broken!
Join ',' ${mylist[@]} = foo,bar,baz,three,four,five by five,six
String_Join '' ${mylist[@]} = foobarbazthreefourfive by fivesix
foreach "${!mylist[@]}":
Element: 0; value: foo
Element: 1; value: bar
Element: 2; value: baz
Element: 3; value: three
Element: 4; value: four
Element: 5; value: five by five
Element: 6; value: six
But don't do this: ${mylist[*]}
Element: foo; value: foo
Element: bar; value: foo
Element: baz; value: foo
Element: three; value: foo
Element: four; value: foo
Element: five; value: foo
Element: by; value: foo
Element: five; value: foo
Element: six; value: foo
Start from element 5 and show a slice of 2 elements:
five\ by\ five|six|
Shift FIRST element [0] (dumped before and after):
declare -a mylist=([0]="foo" [1]="bar" [2]="baz" [3]="three"

Хеши  101

[4]="four" [5]="five by five" [6]="six")
declare -a mylist=([0]="bar" [1]="baz" [2]="three" [3]="four"
[4]="five by five" [5]="six")
Pop LAST element (dumped before and after):
declare -a mylist=([0]="bar" [1]="baz" [2]="three" [3]="four"
[4]="five by five" [5]="six")
declare -a mylist=([0]="bar" [1]="baz" [2]="three" [3]="four" [4]="five by five")
Delete element 2 using unset (dumped before and after):
declare -a mylist=([0]="bar" [1]="baz" [2]="three" [3]="four" [4]="five by five")
declare -a mylist=([0]="bar" [1]="baz" [3]="four" [4]="five by five")

Хеши
Ассоциативные массивы, также известные как хеши или словари, — это
списки, индексами в которых являются строки, а не целые числа. Кроме
прочего, такие массивы очень удобны для подсчета или «уникализации»
(то есть игнорирования или удаления дубликатов) строк.
В отличие от списков, хеши обязательно должны объявляться с помощью команд declare -A, local -A или readonly -A, а при обращении всегда
необходимо указывать индекс. Пример хеша приведен в табл. 7.2.
Таблица 7.2. Пример хеша в bash
Элемент

Значение

myhash[oof]

foo

myhash[rab]

bar

myhash[zab]

baz

Типичные операции с хешами или словарями:
объявление переменной ассоциативным массивом (здесь мы называем переменную «массивом», потому что используем ключ -A);
присваивание переменной одного или нескольких значений;

102  Глава 7. Списки и хеши

вывод всех значений для отладки или повторного использования;
ссылка на одно или на все значения (for или for each);
ссылка на конкретное значение (поиск);
удаление одного или нескольких значений;
удаление всего хеша.
И снова не будем рассуждать обо всех этих операциях, а просто покажем
пример их использования, чтобы вы могли выбрать нужные идиомы,
когда они вам понадобятся (пример 7.3).
Пример 7.3. Примеры операций с хешами в bash: код
#!/usr/bin/env bash
# hashes.sh: примеры операций с хешами в bash
# Автор и дата: _bash Idioms_ 2022
# Имя файла в bash Idioms: examples/ch07/hashes.sh
#_________________________________________________________________________
# Не работает в Zsh 5.4.2!
# Книжные страницы не так широки, как экран компьютера!
FORMAT='fmt --width 70 --split-only'
# Объявление хеша
declare -A myhash



# Хеш ОБЯЗАТЕЛЬНО должен объявляться с помощью
# этой команды, `local -A` или `readonly -A`

# Присваивание значения, обратите внимание на "+" ❷
###myhash=(bar)
# Ошибка: чтобы присвоить значение элементу
# ассоциативного массива,
# необходимо использовать нижний индекс
myhash[a]='foo'
# Добавление первого (не нулевого) элемента
myhash[b]='bar'
# Добавление второго элемента
myhash[c]='baz'
# Добавление третьего элемента
myhash[d]='three'
# Четвертый элемент, отличающийся от значения четвертого
# элемента в примере со списками
myhash[e]='four'
# Добавление пятого элемента
myhash[f]='five by five' # Шестой элемент. Обратите внимание на пробелы
myhash[g]='six'
# Добавление седьмого элемента
# ИЛИ
#myhash=([a]=foo [b]=bar [c]=baz [d]="three" [e]="four" [f]="five by five"
[g]="six")
# Вывод некоторых деталей и содержимого ❸
echo -e "\nThe key count is: ${#myhash[@]} or ${#myhash[*]}"

Хеши  103

echo -e "\nThe length of the value of key [e] is: ${#myhash[e]}"
echo -e "\nDump or list:"
declare -p myhash | $FORMAT
echo -n
"\${myhash[@]}
= " ; printf "%q|" ${myhash[@]}
echo -en
"\n\${myhash[*]}
= " ; printf "%q|" ${myhash[*]}
echo -en "\n\"\${myhash[@]}\" = " ; printf "%q|" "${myhash[@]}"
echo -en "\n\"\${myhash[*]}\" = " ; printf "%q|" "${myhash[*]}"
echo -e " # Broken!" # Предыдущая строка -- ошибочная,
# она не выводит символ перевода строки
# См. `help printf` или раздел "printf для повторного использования или отладки"
# в главе 6. Мы использовали этот код, чтобы показать отдельные слова:
# %q экранирует аргумент так, что его можно повторно использовать
# в качестве входных данных в командной оболочке
# "Объединение" значений ❹
function Join { local IFS="$1"; shift; echo "$*"; } # Односимвольный разделитель!
# Обратите внимание, что Join выше использует "$*", а не "$@"!
echo -en "\nJoin ',' \${myhash[@]} = " ; Join ',' "${myhash[@]}"
function String_Join {
local delimiter="$1"
local first_element="$2"
shift 2
printf '%s' "$first_element" "${@/#/$delimiter}"
# Выводит первый элемент, затем повторно использует формат '%s'
# для отображения остальных элементов (из аргументов функции $@),
# но добавляет префикс $delimiter, "замещая" пустой начальный шаблон (/#)
# значением $delimiter
}
echo -n "String_Join '' \${myhash[@]} = " ; String_Join '' "${myhash[@]}"
# Обход ключей и значений ❺
echo -e "\nforeach \"\${!myhash[@]}\":"
for key in "${!myhash[@]}"; do
echo -e "\tKey: $key; value: ${myhash[$key]}"
done
echo -e "\nBut don't do this: \${myhash[*]}"
for key in ${myhash[*]}; do
echo -e "\tKey: $key; value: ${myhash[$key]}"
done
# Операции со срезами (подмножествами) хеша ❻
echo -e "\nStart from hash insertion element 5 and show a slice of 2 elements:"
printf "%q|" "${myhash[@]:5:2}"
echo '' # Предыдущая инструкция не выводит символ перевода строки
echo -e "\nStart from hash insertion element 0 (huh?) and show a slice of 3
elements:"
printf "%q|" "${myhash[@]:0:3}"

104  Глава 7. Списки и хеши

echo '' # Предыдущая инструкция не выводит символ перевода строки
echo -e "\nStart from hash insertion element 1 and show a slice of 3 elements:"
printf "%q|" "${myhash[@]:1:3}"
echo '' # Предыдущая инструкция не выводит символ перевода строки
#echo -e "\nShift FIRST key [0]:" = не имеет смысла для хешей!
#echo -e "\nPop LAST key:" = не имеет смысла для хешей!
# Удаление ключей ❼
echo -e "\nDelete key c using unset (dumped before and after):"
declare -p myhash | $FORMAT
unset -v 'myhash[c]'
declare -p myhash | $FORMAT
# Удаление всего хеша
unset -v myhash



Необходимые пояснения:

❶ Объявление переменной хешем.
❷ Присваивание значений элементам хеша.
❸ Вывод некоторых деталей и значений; см. пример 4.1.
❹ Две разных функции объединения; см. также раздел «Списки значений, разделенных запятыми» в главе 4.

❺ Обход ключей и значений в списке; см. также раздел «Списки значений, разделенных запятыми» в главе 4.

❻ Операции со срезами (подмножествами) хеша, которые кажутся

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

❼ Удаление ключей.
❽ Удаление всего хеша. Используйте unset с осторожностью, потому
что эта команда может иметь побочные эффекты. Если в файловой
системе имеется файл с именем, совпадающим с именем переменной,
то поддержка подстановки имен файлов в командной оболочке может
привести к неожиданному для вас удалению данных. Чтобы избежать
этого, лучше заключить переменную в кавычки. Еще безопаснее ис-

Хеши  105

пользовать ключ -v, чтобы заставить unset рассматривать аргумент как
переменную, например, unset -v 'list'.
В примере 7.4 показан вывод сценария из примера 7.3.
Пример 7.4. Примеры операций с хешами в bash: вывод
The key count is: 7 or 7
The length of the value of key [e] is: 4
Dump or list:
declare -A myhash=([a]="foo" [b]="bar" [c]="baz" [d]="three"
[e]="four" [f]="five by five" [g]="six" )
${myhash[@]} = foo|bar|baz|three|four|five|by|five|six|
${myhash[*]} = foo|bar|baz|three|four|five|by|five|six|
"${myhash[@]}" = foo|bar|baz|three|four|five\ by\ five|six|
"${myhash[*]}" = foo\ bar\ baz\ three\ four\ five\ by\ five\ six| # Broken!
Join ',' ${myhash[@]} = foo,bar,baz,three,four,five by five,six
String_Join '' ${myhash[@]} = foobarbazthreefourfive by fivesix
foreach "${!myhash[@]}":
Key: a; value: foo
Key: b; value: bar
Key: c; value: baz
Key: d; value: three
Key: e; value: four
Key: f; value: five by five
Key: g; value: six
But don't do
Key:
Key:
Key:
Key:
Key:
Key:
Key:
Key:
Key:

this: ${myhash[*]}
foo; value:
bar; value:
baz; value:
three; value:
four; value:
five; value:
by; value:
five; value:
six; value:

Start from hash insertion element 5 and show a slice of 2 elements:
four|five\ by\ five|
Start from hash insertion element 0 (huh?) and show a slice of 3 elements:
foo|bar|baz|
Start from hash insertion element 1 and show a slice of 3 elements:
foo|bar|baz|

106  Глава 7. Списки и хеши

Delete key c using unset (dumped before and after):
declare -A myhash=([a]="foo" [b]="bar" [c]="baz" [d]="three"
[e]="four" [f]="five by five" [g]="six" )
declare -A myhash=([a]="foo" [b]="bar" [d]="three" [e]="four"
[f]="five by five" [g]="six" )

Пример подсчета слов
Как мы уже говорили, одним из наиболее распространенных применений
хешей является подсчет и/или «уникализация» элементов. Продемонстрируем это на задаче по подсчету слов (пример 7.5).
Пример 7.5. Пример подсчета слов в bash: код
#!/usr/bin/env bash
# word-count-example.sh: Дополнительные примеры работы со списками, хешами
#
и $RANDOM в bash
# Автор и дата: _bash Idioms_ 2022
# Имя файла в bash Idioms: examples/ch07/word-count-example.sh
#_________________________________________________________________________
# Не работает в Zsh 5.4.2!
# См. также: `man uniq`
WORD_FILE='/tmp/words.txt'
> $WORD_FILE ❶
trap "rm -f $WORD_FILE" ABRT EXIT HUP INT QUIT TERM
declare -A myhash



echo "Creating & reading random word list in: $WORD_FILE"
# Создание списка слов для использования в примере
mylist=(foo bar baz one two three four)
# Выбор случайных элементов из списка в цикле ❸
range="${#mylist[@]}"
for ((i=0; i> $WORD_FILE
done
# Запись слов из списка в хеш
while read line; do ❻
(( myhash[$line]++ )) ❼
done < $WORD_FILE ❽



Пример подсчета слов  

echo -e "\nUnique words from: $WORD_FILE"
for key in "${!myhash[@]}"; do
echo "$key"
done | sort

107



echo -e "\nWord counts, ordered by word, from: $WORD_FILE"
for key in "${!myhash[@]}"; do
printf "%s\t%d\n" $key ${myhash[$key]}
done | sort
echo -e "\nWord counts, ordered by count, from: $WORD_FILE"
for key in "${!myhash[@]}"; do
printf "%s\t%d\n" $key ${myhash[$key]}
done | sort -k2,2n





Необходимые пояснения:

❶ Создание временного файла с установкой ловушки (см. раздел «Это
ловушка!» в главе 9) для его удаления.
❷ Хеш обязательно должен объявляться с помощью команды declare

-A

как ассоциативный массив (мы снова называем хеш «массивом», потому
что используется ключ -A).

❸ Получение количества элементов (диапазона случайных чисел).
❹ Выбор случайного элемента списка с помощью переменной $RANDOM
(пример 4.3).
❺ Каждое случайно выбранное слово записывается во временный файл.

Вывод случайно выбранных значений выполняется тремя строками кода
(❸, ❹ и ❺), но то же самое можно реализовать в одной строке, например:
echo "${mylist[$$RANDOM% ${#mylist[@]}]}" >> $WORD_FILE, но понять такой
код через шесть месяцев после его написания будет намного сложнее.

❻ Чтение только что созданного файла. Обратите внимание, что имя
файла указано после ключевого слова done (см. ❽).
❼ Увеличение значения счетчика для уже встречавшегося слова.
❽ Обратите внимание, что имя файла указано после ключевого слова
done, завершающего цикл ❻.

108  Глава 7. Списки и хеши

❾ Обход ключей для вывода списка слов без повторений и без исполь-

зования внешней команды uniq. Обратите внимание на команду sort
после ключевого слова done.

❿ Повторный обход ключей для отображения значений счетчиков слов.
⓫ Последний обход ключей для отображения счетчиков, но на этот раз
с числовой сортировкой по второму полю (sort -k2,2n).
В примере 7.6 показан вывод сценария из примера 7.5.
Пример 7.6. Пример подсчета слов в bash: вывод
Creating & reading random word list in: /tmp/words.txt
Unique words from: /tmp/words.txt
bar
baz
foo
four
one
three
two
Word counts, ordered by word, from: /tmp/words.txt
bar 7
baz 6
foo 4
four 3
one 5
three 4
two 6
Word counts, ordered by count, from: /tmp/words.txt
four 3
foo 4
three 4
one 5
baz 6
two 6
bar 7

В заключение: cтиль и удобочитаемость  

109

В заключение: cтиль и удобочитаемость
В этой главе мы познакомились с массивами bash (списками и хешами)
и показали идиоматические приемы для распространенных случаев их
использования. О списках и хешах в bash можно рассказывать долго,
однако дальнейшее обсуждение этой темы выходит за рамки данной
книги. За дополнительной информацией мы рекомендуем обратиться
к следующим ресурсам:
• https://www.gnu.org/software/bash/manual/h tml_node/Arrays.html#Arrays;
• http://wiki.bash-hackers.org/syntax/arrays;
• http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_10_02.html;
• https://learning.oreilly.com/library/view/bash-cookbook-2nd/9781491975329;
• man uniq;
• man sort.
После определения правильной структуры данных остальной код
пишется практически сам собой. При ошибочном выборе структуры
дальнейшая реализация становится проблемной. В bash есть готовые
строительные блоки для создания простых структур данных, и, освоив
дополнительные знаки препинания, вы с легкостью будете их применять и понимать. Просто помните, что почти во всех случаях следует
использовать [@], а не [*], и, если засомневаетесь, возвращайтесь к нашим примерам в этой главе.

ГЛАВА 8

Аргументы

Некоторые сценарии предназначены для выполнения одной задачи, без
каких-либо вариаций. Другие принимают аргументы: имена файлов или
ключи, определяющие особенности выполнения. Если ваш сценарий
имеет более одного параметра, то вам придется проанализировать аргументы, чтобы гарантировать правильную обработку всех возможных
способов передачи аргументов пользователем. И если подумать, даже
сценарию, выполняющему единственную задачу, может потребоваться
обрабатывать параметр -h (или даже --help). Давайте посмотрим, как
анализировать аргументы, не ухудшая удобочитаемости и простоты
сопровождения сценария.

Ваш первый аргумент
Если сценарий принимает только один параметр, его можно получить,
сославшись на переменную $1. В сценарии могут быть такие операторы,
как echo $1 или cat $1. Но мы не рекомендуем использовать $1 как универсальное имя, потому что оно ничего не говорит читателю о природе
параметра. Для упрощения понимания лучше использовать переменные
с говорящими именами. Например, если в параметре передается имя
файла, выберите для переменной такое имя, как in_file или pathname,
и присвойте ей значение сразу в начале сценария. Как было показано
в главе 4, мы можем задавать значения по умолчанию:

Ваш первый аргумент  

111

filename=${1:-favorite.txt} # Или по умолчанию использовать /dev/null?

Если пользователь не задаст параметр при вызове сценария, то значение
$1 не будет установлено. В предыдущем примере, если сценарий будет
вызван без параметра, переменная filename получит значение по умолчанию favorite.txt.
Что если сценарию нужны два, три или больше параметров? Как нетрудно догадаться, соответствующие аргументы будут храниться в переменных $2, $3 и т. д. И они не будут установлены, если требуемые параметры
не были переданы при вызове сценария.
А что произойдет, если для параметра нет хорошего значения по умолчанию? Или, может быть, сценарий должен завершиться, если пользователь передал недостаточное количество аргументов? Число полученных
аргументов в сценарии содержится в переменной $#. Если значение $#
равно 0, значит пользователь запустил сценарий без аргументов. Если
значение $# равно 1, а сценарию нужны два аргумента, то он может вывести сообщение об ошибке и завершиться (см. раздел «Код выхода»
в главе 9):
if (($# != 2)); then
echo "usage: $0 file1 file2" # $0 -- имя сценария, с которым он был вызван
exit
fi

Приведенный выше код просто проверяет количество полученных аргументов. Но приемы использования этих аргументов в сценарии весьма
разнообразны. Некоторые из них были рассмотрены в главе 2.
Ранее для вывода списка всех аргументов было принято использовать
переменную $*, например в таком варианте: echo $*. Но после появления
поддержки пробелов в именах файлов предпочтительным стал другой
синтаксис.
Имена файлов с пробелами необходимо заключать в кавычки, например "my file", иначе командная оболочка интерпретирует это имя как

112  Глава 8. Аргументы

два отдельных слова. Чтобы сослаться на все аргументы сценария и заключить каждый из них в кавычки, можно использовать конструкцию
"$@" (строка) или "${@}" (список). Ссылка на "$*" даст одну большую
строку в кавычках, содержащую все аргументы. Например, если вызвать
сценарий следующим образом:
myscript file1.dat "alt data" "local data" summary.txt

ссылка "$*" вернет единственное значение "file1.dat alt data local data
summary.txt", тогда как "$@" вернет четыре отдельных слова: file1.dat "alt
data" "local data" summary.txt.
Хотя мы уже говорили об этом в разделе «Кавычки и пробелы» в главе 2,
а затем в главе 7, некоторые сложные особенности bash целесообразно
напомнить еще раз.

Поддержка ключей
Ключи (options) дают возможность изменить выполнение команды.
Классический «идиоматический» способ представления ключей в Unix/
Linux — одна буква с предшествующим дефисом, минусом или тире.
Например, чтобы сообщить команде, что от нее ожидается развернутый
вывод, можно передать ключ -l. А чтобы команда выдавала как можно
меньше выходных данных, можно передать ключ -q.
Не все команды и сценарии поддерживают эти ключи, и не все интерпретируют их одинаково. Например, ключ -q одни команды интерпретируют как выбор «тихого» (от quiet) режима вывода, в других он может
означать «быстро» (quick), а в третьих быть вообще недопустимым. Все
эти особенности основаны, как правило, на традициях.
Традиции стоят того, чтобы их придерживаться, если нет веской причины для отказа. Следование традициям сокращает время обучения, поскольку позволяет использовать знания и опыт, полученные при работе
с другими командами или сценариями. Кроме того, можно применять
те же методы анализа ключей, что и в других командах и сценариях.

Поддержка ключей  

113

Анализ ключей
Для анализа ключей сценария командной оболочки используйте встроенную команду getopts . Ее можно вызывать многократно (обычно
в цикле while), пока не будут получены все ключи. Предполагается, что
ключи предшествуют другим аргументам. getopts распознает ключи,
указанные по отдельности (-a -v), а также сгруппированные вместе
(-av). Также можно указать, что ключ должен сопровождаться дополнительным аргументом. Например, можно потребовать, чтобы ключ
-o сопровождался именем выходного файла и пользователь вызывал
сценарий с параметром -o filename или –ofilename — getopts поддерживает оба варианта.
В примере 8.1 анализируются короткие ключи.
Пример 8.1. Анализ аргументов с помощью getopts: короткие ключи
#!/usr/bin/env bash
# parseit.sh: использование getopts для анализа аргументов
# Автор и дата: _bash Idioms_ 2022
# Имя файла в bash Idioms: examples/ch08/parseit.sh
#_________________________________________________________________________
while getopts ':ao:v' VAL ; do ❶
case $VAL in ❷
a ) AMODE=1 ;;
o ) OFILE="$OPTARG" ;;
v ) VERBOSE=1 ;;
: ) echo "error: no arg supplied to $OPTARG option" ;;
* ) ❹
echo "error: unknown option $OPTARG"
echo " valid options are: aov"
;;
esac
done
shift $((OPTIND -1)) ❺



Рассмотрим особенности сценария:

❶ Цикл while getopts используется, потому что требуется вызвать getopts
несколько раз. Эта команда возвращает истинное значение, когда обнаруживает ключ (дефис, за которым следует любая буква, допустимая или
нет), и ложное, когда дойдет до конца списка параметров. Встроенная

114  Глава 8. Аргументы

команда getopts требует передать ей два слова: список параметров
и имя переменной, в которую она должна поместить очередной найденный ключ. При каждом вызове она будет находить только один
параметр, поэтому мы вызываем ее многократно в цикле while. Строкой ':ao:v' мы сообщаем, что сценарий поддерживает ключи a, o и v.
Двоеточие в начале этой строки указывает, что getopts не будет сообщать об ошибках при обнаружении неподдерживаемого параметра
и оставит его обработку на усмотрение сценария. Двоеточие между o
и v указывает, что ключ o должен сопровождаться дополнительным
аргументом. VAL — имя переменной, куда будет записан очередной
найденный ключ.

❷ После вызова getopts можно использовать оператор case, чтобы определить, какой ключ был найден (подробнее об операторе case рассказывается в главе 3).

❸ Поскольку мы потребовали от getopts не выводить сообщений об ошиб-

ках, мы должны предусмотреть два варианта обработки ошибок. Первый
вариант обрабатывает случаи, когда ключ -o не содержит аргумента. Мы
сообщили команде getopts, что ожидаем аргумент, добавив двоеточие после o в строке ':ao:v'. Если, вызывая сценарий, пользователь не передаст
аргумент, то переменная $VAL получит двоеточие в качестве значения,
а $OPTARG — символ ключа, для которого не был указан обязательный
аргумент (т. е. o).

❹ Второй вариант ошибки — пользователь указал не поддерживаемый
ключ. В этом случае $VAL получит значение '?', а $OPTARG — символ нераспознанного ключа. Этот случай обрабатывается с помощью варианта
(*) в операторе case.

❺ Для контроля последовательности анализа командной строки в пере-

менной $OPTIND сохраняется индекс следующего рассматриваемого аргумента. Когда все аргументы будут проанализированы, getopts вернет
ложное значение — и цикл while завершится. После этого выполняется
команда shift $OPTIND -1, чтобы исключить из дальнейшего рассмотрения
все аргументы, связанные с ключами.

Длинные ключи  

115

Независимо от того, каким образом вызывается сценарий, с помощью ли
myscript -a -o xout.txt -v file1 или просто myscript file1, после выполнения команды shift переменная $1 будет хранить значение file1, потому
что промежуточные ключи и их аргументы будут удалены. Аргументами
сценария теперь будут считаться все оставшиеся аргументы без ключей.

Длинные ключи
Иногда одной буквы недостаточно, требуются полные слова или даже фразы для описания ключа. Встроенная функция getopts поддерживает и их.
Длинные ключи должны как-то отличаться от нескольких однобуквенных ключей, объединенных вместе. Например, означает ли ключ -last
объединение ключей -l -a -s -t или это длинный ключ — слово last?
Поэтому длинные ключи начинаются с двух дефисов (т. е. в рассмотренном случае длинный ключ должен передаваться как --last).
Чтобы использовать getopts для анализа длинных ключей, нужно добавить в список параметров знак минус и двоеточие, а затем еще один
оператор case для распознавания каждого из длинных ключей. Двоеточие должно включаться в список параметров, даже если длинный ключ
не принимает аргументов (мы объясним эту особенность в следующих
разделах).
В примере 8.2 приведен сценарий, созданный на основе предыдущего
примера, но анализирующий два дополнительных длинных ключа:
--amode и --outfile. Первый означает то же, что и короткий ключ -a;
второй действует так же, как -o, принимающий аргумент. Имя длинного
ключа и соответствующий ему аргумент могут передаваться сценарию
одним из двух способов: как одно слово со знаком равенства в качестве разделителя или как два слова. Например, вызов с параметром
--outfile=file.txt или --outfile file.txt сообщает сценарию имя выходного файла. Команда getopts и второй оператор case в примере 8.2
смогут обработать как короткие, так и длинные ключи.

116  Глава 8. Аргументы

Пример 8.2. Анализ аргументов с помощью getopts: длинные ключи
#!/usr/bin/env bash
# parselong.sh: использование getopts для анализа аргументов, включая длинные
# Автор и дата: _bash Idioms_ 2022
# Имя файла в bash Idioms: examples/ch08/parselong.sh
#_________________________________________________________________________
# Длинные ключи: --amode
#
и --outfile filename или --outfile=filename
VERBOSE=':' # По умолчанию выключен
while getopts ':-:ao:v' VAL ; do ❶
case $VAL in
a ) AMODE=1 ;;
o ) OFILE="$OPTARG" ;;
v ) VERBOSE='echo' ;;
#-------------------------------------------------------- ) # Этот раздел добавлен для поддержки длинных ключей
case $OPTARG in
amode
) AMODE=1 ;;
outfile=* ) OFILE="${OPTARG#*=}" ;; ❸
outfile
) ❹
OFILE="${!OPTIND}" ❺
let OPTIND++ ❻
;;
verbose ) VERBOSE='echo' ;;
* )
echo "unknown long argument: $OPTARG"
exit
;;
esac
;;
#-------------------------------------------------------: ) echo "error: no argument supplied" ;;
* )
echo "error: unknown option $OPTARG"
echo " valid options are: aov"
;;
esac
done
shift $((OPTIND -1))



Как работает поддержка длинных ключей? Давайте разберемся:

❶ Команда getopts разрабатывалась для поддержки односимвольных

ключей (например, -a). Добавление знака минус в список ее параметров
означает, что два дефиса (--) будут распознаваться как допустимый ключ.

Длинные ключи  

117

❷ Любые символы, следующие за двумя дефисами, будут считаться
«аргументом» ключа и помещаться в переменную $OPTARG. Для сопоставления значения $OPTARG с длинными именами ключей используется
вложенный оператор case.
❸ Как обрабатываются длинные ключи, например

--outfile , которые должны сопровождаться аргументами? Если аргумент передан
со знаком равенства, например --outfile=my.txt, то getopts присвоит
всю строку (после --) переменной OPTARG. Извлечь аргумент из строки
можно с помощью механизма расширения параметров (см. раздел «Расширение параметров» в главе 4), удалив (\#) все символы (\*) до знака
равенства (=) включительно. В результате останутся только символы,
следующие за знаком равенства, т. е. собственно аргумент. Также для
извлечения аргумента можно было бы явно указать удаляемую строку:
OFILE="${OPTARG#outfile=}".

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

вариант outfile в операторе case. Здесьиспользуется переменная $OPTIND,
в которой getopts сохраняет текущее местоположение в анализируемой
строке параметров.

❺ Аргумент с именем файла извлекается косвенно с помощью ${!OPTIND}.

Это работает следующим образом. В переменной $OPTIND хранится индекс следующего аргумента, который должен быть обработан getopts.
Восклицательный знак (!) сообщает о косвенной ссылке, когда значение $OPTIND используется как имя извлекаемой переменной. Например,
если ключ --output был третьим аргументом, встреченным командой
getopts , то в этот момент $OPTIND будет иметь значение 4, и, следовательно, ${!OPTIND} вернет значение ${4}, т. е. следующий аргумент
с именем файла.

❻ В заключение цикла нужно снова «сдвинуть» $OPTIND, чтобы перейти
к еще не обработанным ключам.

Остальная часть сценария осталась неизменной.

118  Глава 8. Аргументы

HELP!
В предыдущих примерах есть явное упущение — отсутствие справки или
подсказок, описывающих ключи и аргументы. Действительно, ключей -h
и/или --help в этих примерах нет. Мы настоятельно рекомендуем предусматривать вывод по запросу справочной информации. Соответствующие ключи легко согласуются с большинством других инструментов
и реализовать их несложно.
Для простых сценариев подойдет решение, показанное в примере 8.3.
Пример 8.3. Справка на скорую руку
PROGRAM=${0##*/} # Версия `basename` на языке bash ❶
if [ $# -lt 2 -o "$1" = '-h' -o "$1" = '--help' ]; then ❷
# Отступы в строках, следующих за EoH (End-of-Help), оформлены табуляциями!
cat &2
echo "Batch size and count:
$BATCH_SIZE / $batch_count" 1>&2
echo "Sleep seconds per node: $SLEEP_SECS_PER_NODE" 1>&2
echo "Sleep seconds per batch: $SLEEP_SECS_PER_BATCH" 1>&2
echo '' 1>&2



node_counter=0
batch_counter=0
# Прочитать данные...
&& если данные доступны в $HOSTS_FILE
while mapfile -t -n $BATCH_SIZE nodes && ((${#nodes[@]})); do
for node in ${nodes[@]}; do ❹
echo "node $(( node_counter++ )): $node"
sleep $SLEEP_SECS_PER_NODE
done
(( batch_counter++ ))
# Чтобы не попасть сюда ПОСЛЕ чтения последнего (неполного) пакета...
[ "$node_counter" -lt "$node_count" ] && {
# Для вывода также можно использовать mapfile -C Call_Back -c $BATCH_SIZE,
# но тогда обратный вызов выполняется заранее, поэтому возможна задержка
# вывода
echo "Completed $node_counter of $node_count nodes;" \
"batch $batch_counter of $batch_count;" \
"sleeping for $SLEEP_SECS_PER_BATCH seconds..." 1>&2
sleep $SLEEP_SECS_PER_BATCH
}
done < $HOSTS_FILE

Разберем код:

❶ Это не «бесполезное» использование cat. Обычно wc -l выводит , но нам нужно только , а получить

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

Чтение файлов  

129

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

❸ Целочисленная арифметика; см. п. ❷.
❹ Мы не заключили в кавычки ${nodes[@]} в цикле for, но это допустимо в данном случае, потому что мы читаем имена хостов, в которых не
может быть пробелов. Однако лучше выработать привычку всегда использовать кавычки.
Результат выполнения этого сценария приведен в примере 9.4.
Пример 9.4. Более сложный пример использования mapfile: вывод
Nodes
Batch
Sleep
Sleep

to process:
size and count:
seconds per node:
seconds per batch:

10
4 / 2
1
1

node 0: node0
node 1: node1
node 2: node2
node 3: node3
Completed 4 of 10 nodes; batch 1 of 2; sleeping for 1 seconds...
node 4: node4
node 5: node5
node 6: node6
node 7: node7
Completed 8 of 10 nodes; batch 2 of 2; sleeping for 1 seconds...
node 8: node8
node 9: node9

Источники дополнительной информации:
• help mapfile;
• help readarray (в этом случае вернется справка для mapfile);
• описание приемов эффективного использования mapfile для
очистки корзины AWS S3: https://oreil.ly/xie4t.

130  Глава 9. Файлы и не только

Метод «грубой силы»
Пример 9.5 демонстрирует, как прочитать файл в память целиком.
Пример 9.5. Пример чтения файла методом «грубой силы»
for word in $(cat file); do
echo "word: $word"
done
### Или, убрав "бесполезный" cat
for word in $(< file); do
echo "word: $word"
done

Изменяем $IFS при чтении файлов
— это аббревиатура от Internal Field Separator (внутренний разделитель полей). Переменная $IFS применима во всех случаях, когда
требуется разбить строку на слова. По умолчанию используется конструкция или IFS=$' \t\n'
с механизмом экранирования $'', соответствующим стандарту ANSI C1.
Она служит основой для многих идиом bash. Если изменить $IFS, не
понимая последствий, возможны неожиданности. В частности, замена
первого символа в значении $IFS , который по умолчанию является
пробелом и используется в расширениях слов, может привести к хаосу.
Если вы уверены, что вам нужно изменить значение $IFS, сделайте это
либо в функции, используя локальную переменную, либо локально по
отношению к команде (раздел «Локальные переменные» в главе 10).
Пример 9.6 иллюстрирует, как правильно изменять $IFS.
IFS

Пример 9.6. Изменение IFS при использовании команды read: код
#!/usr/bin/env bash
# fiddle-ifs.sh: Манипуляции с $IFS при чтении файлов
# Автор и дата: _bash Idioms_ 2022
# Имя файла в bash Idioms: examples/ch09/fiddle-ifs.sh
#_________________________________________________________________________

1

https://oreil.ly/00l9N.

Изменяем $IFS при чтении файлов  

131

# Создать тестовый файл (слово word сокращено, чтобы ширина вывода
# не превышала 80 символов)
IFS_TEST_FILE='/tmp/ifs-test.txt'
cat /tmp/previous-report.log
cut -f1 /path/to/current-report.log | sort -u > /tmp/current-report.log
diff /tmp/previous-report.log /tmp/current-report.log
rm /tmp/previous-report.log /tmp/current-report.log

То же самое можно реализовать, используя так называемые «имитации
файлов» (pretend files):
diff 0 не произошел выше
: