Подготовка эффективной среды для написания bash сценариев +22




Bash, он же возрождённый shell, является по прежнему, одним из самых популярных командных процессоров и интерпретаторов сценариев. Как бы его ненавидели и не пытались заменить, всё равно он присутствует вокруг нас и никуда не собирается исчезать. Если вам приходится писать bash скрипты или вы только планируете этим заняться, данная статья написана для вас.

Статья несет исключительно рекомендательный характер и затрагивает в первую очередь bash, но также будет полезна и для работы с совместимыми оболочками, такими как: sh, ash, csh, ksh и tcsh.

На данный момент, тема bash скриптинга не менее актуальна чем 10 или 20 лет назад. Хотя большинство дистрибутивов Linux перешли на Systemd или аналоги, а System V с скриптами для запуска служб ушел на пенсию, многое по прежнему реализовано при помощи скриптов и многие из них - это shell скрипты. В ключевых и популярных дистрибутивах, bash является оболочкой по умолчанию, и это неспроста, при помощи него легко автоматизировать рутину. С появлением docker, а после и kubernetes, тема bash скриптов стала только актуальнее, количество всевозможных docker-entrypoint.sh, Job, CronJob и initContainers растет, а реализуются они чаще всего при помощи bash. Инструкция RUN в Dockerfile, вовсе внесла огромный вклад в мировой запас shell строк.

Это несложно, но необходимо знать некоторые основы bash скриптинга. Поехали!


Текстовый редактор

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

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

  • Консольные текстовые редакторы. Vim, Emacs и Nano - классическая троица, сейчас уже редко кто использует на рабочих станциях как основной инструмент, но vi и nano незаменимы для быстрого редактирования файлов в удаленных ssh сессиях. Если вы еще не работали с ним, рекомендую освоить такие вещи как поиск, замена и форматирование, хотя бы в nano.

  • Графические текстовые редакторы. Mousepad, Gedit, Notepad++ и т.п. Легковесные редакторы, с подсветкой синтаксиса, автозаменой и прочим, что уже есть в консольных редакторах, но они всё еще не являются полноценной интегрированной средой разработки.

  • IDE. Geany, Atom, IntelliJ IDEA, Sublime Text и Visual Studio Code - это уже полноценные и расширяемые среды разработки. Долгие годы я пользовался Geany и пробовал все перечисленные варианты, но только с появлением VSCode мне удалось сменить основную IDE для большинства задач.

ℹ️ Половина статьи затрагивает конфигурацию параметров и расширения для Visual Studio Code. Хотя, эта IDE может быть не в вашем вкусе, но информация приведенная в статье будет полезна в академических целях, а полученные знания, вы можете адаптировать под свое любимое окружение.

Альтернативные редакторы

Существует как минимум три альтернативных среды разработки для написания bash скриптов:

  • Специализированная IDE BashEclipse основанная на Eclipse.

  • В IntelliJ IDEA можно добиться расширенной поддержки bash скриптинга путем установки расширений Shell Script, ShellCheck и BashSupport.

  • Bash Kernel для Jupyter Notebook.


Настройка окружения

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

Ширина строк кода

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

Добавим нижеприведенную конфигурацию в файл настроек settings.json для Visual Studio Code.

Как найти settings.json
  1. Откройте Visual Studio Code

  2. Нажмите F1, чтобы открыть командную панель

  3. Введите в открывшуюся панель «open settings»

  4. Вам представлены два варианта, выберите «Open Settings (JSON)»

{
  "[shellscript]": { // настройки применимые только для shellscript файлов
    "editor.rulers": [ // вертикальные лини подсветки столбцов 72, 80, 132 
      { "column": 72,  "color": "#1e751633", },
      { "column": 80,  "color": "#c2790b99", },
      { "column": 132, "color": "#a10d2d99" }
    ],
    "editor.minimap.maxColumn": 132, // ширина миникарты
    "editor.wordWrap": "off", // запрещаем перенос строк
  },
  // ... прочие настройки
}
Краткий ответ, почему именно 72, 80 и 132 символа

Вы можете поблагодарить перфокарту IBM 1928 года за этот предел - в ней было 80 столбцов. Почему 80? Дело в том, что типичный шаг пишущей машинки 10-12 символов на дюйм, а это приводит к документам шириной от 72 до 90 символов, в зависимости от размера полей. После этого ранние телетайпы, а затем видео-терминалы использовали 80 столбцов, а затем 132 столбца в качестве стандартной ширины. Сейчас же, к примеру, пропорции окна эмулятора терминала по умолчанию остаются равными 80x24.

Три файла с шириной в 80 символов в Full HD разрешении и 12 размером шрифта, уже немного не помещаются на экран.
Три файла с шириной в 80 символов в Full HD разрешении и 12 размером шрифта, уже немного не помещаются на экран.

Но ведь сейчас же не 1928 год! Да, но эргономика чтения файлов в 80 столбцов гораздо выше, а при сравнении двух файлов мы используем в два раза большую ширину (бывают еще diff-ы трех состояний). Все еще используются удаленные параллельные терминалы в гипервизорах и KVM-свитчи в серверных, и порой приходится что-то быстро поправить через них, находясь в сессии с разрешением 1024x768, а может и меньшим.

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

Отступы и окончание строк

Одной из проблем, которую я встретил 15 лет назад, когда только знакомился с bash и наблюдаю по сей день, это CRLF (\r\n или 0x0D0A) в файлах сценариев. Источником проблемы, чаще всего является копипаста bash скриптов в windows системах, но также это может быть и просто по невнимательности. Давайте настроим завершение сток при помощи LF.

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

{
  "[shellscript]": { // настройки применимые только для shellscript
    "files.eol": "\n", // явно зададим LF формат EOL
    "files.insertFinalNewline": true, // завершаем все файлы новой строкой
    "files.trimFinalNewlines": true, // удалим лишние новые строки в конце файла
    "files.trimTrailingWhitespace": true, // удалим лишние пробелы в конце строк
    "editor.renderWhitespace": "boundary", // отобразим два и более пробелов
    "editor.insertSpaces": true, // отступы делаем пробелами
    "editor.tabSize": 2, // размер отступа в два пробела
  },
  // ... прочие настройки
}
Чем страшен CRLF?

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

Давайте рассмотрим на примере, у нас есть простейший скрипт:

#!/usr/bin/env bash

set -eu

printf '%s ' "Hi ${USER:-John Doe}! Today is"
LANG=en date

Сохраним его в файл test-eol.sh, сделаем его исполняемым chmod +x ./test-eol.sh и проверим работу:

# ./test-eol.sh
Hi woozymasta! Today is Mon Oct 18 00:48:13 MSK 2021

Всё хорошо, давайте заменим LF на CRLF, можно воспользоваться командой sed $'s/$/\r/' -i test-eol.sh и запустим сценарий еще раз:

# ./test-eol.sh
/usr/bin/env: 'bash\r': No such file or directory

Скрипт упал с ошибкой о том, что файла bash\r не существует, утилита env приняла на вход строку как есть. И это хорошо, что скрипт упал, ведь могло произойти что-то более непредвиденное. Давайте обойдем использование shebang, передав путь к скрипту как аргумент для bash:

# bash ./test-eol.sh
./bash-eol.sh: line 2: $'\r': command not found
: invalid optionine 3: set: -
set: usage: set [-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]
./bash-eol.sh: line 4: $'\r': command not found
 ./bash-eol.sh: line 6: $'date\r': command not found

Как видно, теперь были обработаны все инструкции скрипта. Правда set -e не смог выполниться, и каждая команда сценария, смогла выполниться с ненулевым кодом возврата.

В этом примере, ничего страшного не произошло, но, что если у вас был бы такой скрипт:

#!/usr/bin/env bash

set -eu

printf '%s ' "Hi ${USER:-John Doe}! Today is"
LANG=en date || \
  { rm -rf / --no-preserve-root; echo "you will not pass"; }; echo Done

Осторожно! Для проверки этого скрипта, всё же лучше замените rm -rf / --no-preserve-root , к примеру на touch test

Зачем нужна пустая строка в конце файла?

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

Текстовый файл в Unix состоит из серии строк, каждая из которых заканчивается символом новой строки \n. Таким образом, файл, который не является пустым и не заканчивается новой строкой, не является текстовым файлом.

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

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

Автосохранение

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

Просто помните об этом, и не включайте автосохранение при написании bash скриптов. К сожалению параметр files.autoSave не поддерживается для выборочных типов файлов, а устанавливается глобально на всё окружение.

Проверим на практике редактирование уже исполняющегося скрипта

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

А теперь рассмотрим простой доказательный пример, создадим скрипт test.sh работающий две секунды:

#!/usr/bin/env bash

sleep 1s
echo one
sleep 1s
echo two

Запустим скрипт и по итогу выполнения напечатаем код выхода, объединим это в группу и запустим в отдельном потоке, а пока он отработает половину отведенного ему времени, допишем в него еще одну команду exit 42 :

{ ./test.sh && echo $?; } & sleep 1s; echo 'exit 42' >> ./test.sh; wait

ℹ️ Если однострочники у вас вызывают некоторое волнение, воспользуйтесь сервисом explainshell.com, он поможет на первых порах разбирать такие конструкции.

И вот итог:

[1] 1208831
one
two
[1]+  Выход 42           { ./test.sh && echo $?; }

Но допустим автосохранение отключать нельзя, или вы сами на автомате нажали Ctrl+S, можно как-то предостеречь это поведение?

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

cat ./test.sh | bash -s - "${args[@]}"

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


Пожалуй это все параметры для редактора, которые хотелось осветить. Для удобства настройки, все параметры которые были внесли в settings.json приведены ниже:

Все параметры в settings.json для shellscript
{
  "files.autoSave": "off",
  "[shellscript]": {
    "files.eol": "\n",
    "files.insertFinalNewline": true,
    "files.trimFinalNewlines": true,
    "files.trimTrailingWhitespace": true,
    "editor.renderWhitespace": "boundary",
    "editor.insertSpaces": true,
    "editor.tabSize": 2,
    "editor.tabCompletion": "on",
    "editor.wordWrap": "off",
    "editor.rulers": [
      {
        "column": 72,
        "color": "#1e751633",
      },
      {
        "column": 80,
        "color": "#c2790b99",
      },
      {
        "column": 132,
        "color": "#a10d2d99"
      }
    ],
    "editor.minimap.maxColumn": 132,
  }
}

Утилиты и расширения

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

Модульность IDE, это большой плюс, главное не перестараться
Модульность IDE, это большой плюс, главное не перестараться

ShellCheck

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

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

Для проверки сценария достаточно выполнить:

shellcheck /path/to/script.sh
Пример результата работы shellcheck
In /path/to/script.sh line 5:
echo $none
     ^---^ SC2154: none is referenced but not assigned.
     ^---^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
echo "$none"


In /path/to/script.sh line 6:
. ./test
  ^----^ SC1091: Not following: ./test was not specified as input (see shellcheck -x).

Но гораздо нагляднее, будет видеть все предупреждения и подсказки в самой IDE. Для этого установим расширение ShellCheck:

ext install timonwong.shellcheck
Пример обнаружения проблем, с сылками на документацию и возможностью хотфикса
Пример обнаружения проблем, с сылками на документацию и возможностью хотфикса

BASH Debugger

BASH Debugger - это внешний отладчик для bash, который следует синтаксису команды gdb.

К сожалению в большинстве дистрибутивов пакет или отсутствует, или имеет очень старую версию, по этому соберем проект из исходников. Скачаем последнюю версию или клонируем с зеркала на github и собираем:

tar xf bashdb-5.0-1.1.2.tar.gz
cd bashdb-5.0-1.1.2/
./configure
make

sudo make install
# можно взять один бинарь и обойтись без make install
# если работать c bashdb будем только из vscode
# cp bashdb ~/.local/bin/

Теперь для запуска отладки скрипта выполним команду:

bash --debugger -- /path/to/script.sh
# или
bashdb /path/to/script.sh
Пример результата работы bashdb
bash debugger, bashdb, release 5.0-1.1.2

Copyright 2002-2004, 2006-2012, 2014, 2016-2019 Rocky Bernstein
This is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.

(/path/to/script.sh:5):
5:	echo $none
bashdb<0> backtrace
->0 in file `./bash-eol.sh' at line 5
##1 main("/usr/share/bashdb/bashdb-main.inc") called from file `/path/to/script.sh' at line 0
bashdb<1> debug
Debugging new script with /usr/bin/bash --init-file /tmp/bashdb_profile_1067091 --debugger echo 
/usr/bin/echo: /usr/bin/echo: не удалось запустить двоичный файл
bashdb<2> continue

/path/to/script.sh: строка 6: ./test: Нет такого файла или каталога
(standard_in) 1: syntax error
hi
Done 

Но это не так удобно, как расставлять точки останова в IDE, по этому установим расширение Bash-debug:

⚠️ Внимание! Для работы требует наличия bashdb в системе

ext install rogalmic.bash-debug
Точки останова в bash сценариях
Точки останова в bash сценариях

Shell Format

Shfmt - утилита для форматирования shell сценариев. Установим последний релиз и попробуем на практике:

# Форматировать скрипт с настройками по умолчанию, вывод направить файл
shfmt /path/to/script.sh > /path/to/script-formated.sh
# Перезаписать файл, использовать отступ в два пробела
shfmt -w -i 2 ~/desktop/bash-eol.sh

Очередь расширения, установим Shell Format:

ext install foxundermoon.shell-format

Нам стало доступно форматирование файлов shellscript, и им уже можно воспользоваться. Для этого вызовем палитру команд, нажав F1, введем в поле «format document» и выберем этот же пункт.

Пример форматирования документа
Пример форматирования документа

Помимо этого, вы можете настроить автоматическое форматирование документов, при сохранении файла, параметр editor.formatOnSave.

ℹ️ Увы расширение не позволяет форматировать выделенный блок кода, в связи с этим недоступен параметр editor.formatOnPaste , позволяющий форматировать код вставляемый из буфера обмена.

Bash Language Server

Bash Language Server - языковой сервер для интеграции в множество различных IDE. Установка языкового сервера приносит нам поведение среды разработки, как у больших языков программирования, такие возможности как: поиск ссылок, переход к объявлению, автодополнение, документация и т.п.

Для VSCode достаточно установить расширение Bash IDE:

ext install mads-hartmann.bash-ide-vscode
Пример отображения ссылок на функцию и автодополнения
Пример отображения ссылок на функцию и автодополнения

ℹ️ Расширение также поддерживает интеграцию с explainshell, но для этого вам понадобится держать запущенным сервер explainshell, а на выходе вы не получите всей той магии, что доступна на сайте explainshell.com. В связи с этим интеграцию считаю сомнительной, и у себя не использую.

Shell Completion

Работая с bash как оболочкой, во многих моментах помогает автодополнение по TAB, так вот для VSCode есть возможность дополнять аргументы для команд, реализуется это при помощи расширения Shell Completion. Давайте проверим:

ext install tetradresearch.vscode-h2o
Пример автодополнения ключей к утилитам
Пример автодополнения ключей к утилитам

Manpages

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

ext install meronz.manpages

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

Пример просмотра руководства jq с свернутыми заголовками
Пример просмотра руководства jq с свернутыми заголовками

ShellMan

Shellman - наверное единственная совместимая с ShellCheck коллекция сниппетов для bash. Будет полезно как новичкам, для более быстрого знакомства с скриптами, так и бывалым разработчикам позволит сэкономить время на написание рутинных конструкций. В магазине расширений доступно около десятка расширений с снипетами для shell скриптов наряду с Shellman, при желании вы можете комбинировать их.

Установка расширения:

ext install Remisa.shellman

Подробно ознакомится с возможностями и советами как пользоваться ShellMan вы можете в книге shellman-ebook.

Code Runner

Code Runner - расширение, позволяющее выполнять произвольный блок кода в самой IDE, для этого достаточно выделить необходимые строки и нажать CTRL+ALT+N, или вызвать данную функцию из контекстного меню, или палитры команд. Это заметно ускорит процесс написания скриптов.

ext install formulahendry.code-runner
Демонстрация работы (GIF 5МБ)
Пример исполнения определенных строк кода
Пример исполнения определенных строк кода
Пример, как можно писать скрипты без походов в терминал для тестирования
Пример, как можно писать скрипты без походов в терминал для тестирования

Hadolint

Hadolint - это, пожалуй лучший линтер для Dockerfile. Почему он оказался в этом списке? Всё довольно просто, в Dockerfile имеется инструкция RUN в которой размещается shell скрипт, а Hadolint помимо общей проверки синтаксиса файла, также использует ShellCheck для проверки этих скриптов.

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

hadolint ./Dockerfile

И установим расширение Hadolint в VSCode:

⚠️ Внимание! Для работы требует наличия hadolint в системе

ext install exiasr.hadolint
ShellCheck проверки работают в секции RUN в Dockerfile
ShellCheck проверки работают в секции RUN в Dockerfile

И как бонус, для подсветки синтаксиса shell скриптов в RUN секции Dockerfile, можно воспользоваться расширением Better Dockerfile Syntax.

Txt Syntax

Еще одно вспомогательное расширение Txt Syntax, напрямую не влияющее на bash скрипты, но позволяет выделить текстовые файлы (.txt, .out .tmp, .log, .ini, .cfg ...) и предоставить общие служебные инструменты для текстовых документов. Shell сценарии часто опираются на всевозможные текстовые файлы, и будет полезно упростить работу с ними в IDE.

ext install xshrim.txt-syntax

ℹ️ Данное расширение помогает работать расширению manpages, а именно складывать и раскладывать заголовки в документах справки.

Better Shell Syntax

И в завершении списка, расширим подсветку синтаксиса. По умолчанию подсветка не настолько хороша как могла быть, и расширение Better Shell Syntax пытается исправить это, позволяя вашей теме лучше раскрашивать код.

ext install jeff-hykin.better-shellscript-syntax

ℹ️ Расширение не будет работать с стандартной темой (не будет эффекта), но всё будет хорошо в таких темах как: Material Theme, Gruvbox, XD Theme и подобных.

При этом может не очень хорошо работать с вашей любимой, нестандартной темой оформления, перед использованием, проверьте всё ли вас устраивает.

Примеры
Пример подсветки с использованием Better Shell Syntax и теме оформления Gruvbox
Пример подсветки с использованием Better Shell Syntax и теме оформления Gruvbox
Пример подсветки с использованием Better Shell Syntax и теме оформления Community Material Theme
Пример подсветки с использованием Better Shell Syntax и теме оформления Community Material Theme

Вот мы и закончили с обзором утилит и расширений. Последние два (Txt Syntax и Better Shell Syntax) несут больше косметический характер, и их можно смело пропустить, чего не могу сказать про весь оставшийся список, рекомендую хотя бы попробовать их на практике.

Для удобства установки, все расширения собраны в один пакет Shell script IDE, правда бинарные зависимости (bashdb и hadolint) придется устанавливать самостоятельно.

ext install woozy-masta.shell-script-ide

Отладка

Хорошо когда настроенная IDE есть под рукой, но не всегда бывает так, к примеру мы работаем на удаленном сервере или в контейнере. По этому затронем тему настройки окружения для отладки и немного коснемся её самой.

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

Еще немного про ключи для отладки

Параметр отладки может быть установлен в произвольном месте в теле скрипта. Для отладки определенного блока кода, установим перед кодомset -x , а для выхода из отладки при достижении конца отлаживаемого блока, обратим параметр вызвав set +x .

Минус используется для активации опций оболочки, а плюс для деактивации. Пусть это вас не смущает.

Параметры которые вам скорее всего понадобятся для отладки:

set -f

set -o noglob

Отключить получение имени файла с использованием метасимволов (подстановка).

set -v

set -o verbose

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

set -x

set -o xtrace

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

set -n

set -o noexec

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

Также длинные параметры следующие за set -o могут быть переданы через переменную SHELLOPTS или используя родную для bash команду shopt.

В shopt включение или отключение опций происходит при помощи флагов:

  • -s (set) - установить опцию;

  • -u (unset) - отключить опцию.

Для того что бы отобразить текущие настройки параметров, выполните set -o или shopt

Для экспериментов, давайте создадим простой скрипт.

Скрипт test.sh
#!/usr/bin/env bash

set -eu

function print-msg () {
  printf '%b%-20s%b' "${colors[${1:-0}]}" "${@:2}" "${colors[0]}"
}

function random-color-echo() {
  print-msg $((1 + RANDOM % $((4 - 1)))) "${*:-}"
}

function msg () {
  random-color-echo "Hi ${*:-}!"
}

colors=(
  "$(tput sgr0)"    # reset
  "$(tput setaf 1)" # red
  "$(tput setaf 2)" # green
  "$(tput setaf 3)" # yellow
  "$(tput setaf 4)" # blue
)

for item in {"Bob","Alice"}; do
  echo "$({ msg "$item"; ( date '+%s%N' ); } & wait)"
done

echo 'Done'

И выполним его при помощи bash -x ./test.sh или добавив set -x в начало скрипта:

Пример работы стандартной трассировки вызовов bash
Пример работы стандартной трассировки вызовов bash

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

Для небольших блоков логики этого зачастую достаточно, но, что если хочется большего? И первое, что мы можем сделать, это добавить необходимую информацию в параметр PS4:

# Levels of indirection and time
PS4='+\011\[\e[3;34m\]\t\[\e[0m\]'
# User ID [Effective user ID]: Groups of user is a member
PS4+=' \[\e[0;35m\]$UID[$EUID]:$GROUPS\[\e[0m\] '
# Shell level and subshell
PS4+='\011\[\e[1;31m\]L$SHLVL:S$BASH_SUBSHELL\[\e[0m\]'
# Source file
PS4+=' \[\e[1;33m\]${BASH_SOURCE:-$0}\[\e[0m\]'
# Line number
PS4+='\[\e[0;36m\]#:${LINENO}\[\e[0m\]'
# Function name
PS4+='\011\[\e[1;32m\]${FUNCNAME[0]:+${FUNCNAME[0]}(): }\[\e[0m\]'
# Executed command
PS4+='\n# '
export PS4

ℹ️ Объявить PS4 вы можете в своем ~/.bashrc и он будет с вами постоянно, или определить свой формат отладки непосредственно в теле самого скрипта, или временно экспортировать изменения на время жизни оболочки bash.

О назначении параметров: PS0, PS1, PS2, PS3 и PS4
  • PS0 - Значение этого параметра раскрывается и отображается интерактивными оболочками после прочтения команды и до ее выполнения. Т.е. это будет напечатано перед исполнением каждой команды, по умолчанию не установлено.

  • PS1 - Значение этого параметра раскрывается и используется в качестве основной строки приглашения. Это ваше стандартное приветствие user@host:~

  • PS2 - Значение этого параметра раскрывается, как и в случае с PS1, и используется в качестве дополнительной строки приглашения.

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

  • PS4 - Значение этого параметра расширяется, как в случае с PS1, и значение печатается перед отображением каждой команды bash во время трассировки выполнения. Первый символ расширенного значения PS4 при необходимости повторяется несколько раз, чтобы указать несколько уровней косвенного обращения. По умолчанию +

И снова запустив скрипт, мы увидим уже немного другой результат:

Расширенный вывод информации при трассировке вызовов bash
Расширенный вывод информации при трассировке вызовов bash

Давайте разберем этот пример, а в дальнейшем вы сами сможете реализовать удобный вывод отладочной информации под ваши нужды.

  • + - Первый символ, отображает уровни косвенного обращения к командам, эта часть осталась как в оригинальном PS4.

  • \t - Текущее время, полезно для изучения тайминга команд, может быть заменено к примеру командой date '+%x %X:%N %z' для более подробного информирования, включая отображение наносекунд.

  • $UID[$EUID]:$GROUPS - Выведем ID и эффективный ID пользователя, перечисляем группы, членом которых является текущий пользователь. Это будет полезно для скриптов выполняющих действия от разных пользователей.

  • L$SHLVL:S$BASH_SUBSHELL - Отображения уровня оболочки, и уровня вложенной подоболочкой. Когда вы запускаете команду в оболочке, она запускается на уровне, называемом уровнем оболочки. Внутри оболочки вы можете открыть другую оболочку, которая делает её подоболочкой, или оболочку, которая её открыла.

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

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

  • ${BASH_SOURCE:-$0} - Имя исполняемого файла или функции.

  • #:${LINENO} - Номер трассируемой строки.

  • ${FUNCNAME[0]:+${FUNCNAME[0]}(): } - Имя функции в рамках которой происходит исполнение.

ℹ️ Подробную информацию о параметрах вы всегда найдете в man bash разделах PROMPTING и PARAMETERS/Shell Variables

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

Для этого мы создадим ссылку для файлового дескриптора с номером 3 на файл debug_$0.log где $0 это имя bash сценария, а переменной BASH_XTRACEFD передадим номер нашего нового дескриптора.

set -x
exec 3> "debug_$0.log"
BASH_XTRACEFD="3"

Также имеется возможность перенаправить вывод не в файл, а в утилиту, к примеру отправив сообщения утилите logger мы сможем обратится к журналу при помощи командыjournalctl -t test.sh.

set -x
exec 3> >(logger -t "$0")
BASH_XTRACEFD="3"

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

function some () {
  { local -; set +x; } 2>/dev/null
  echo 'Do some stuff'
}

На тот случай если стандартной трассировки bash вам недостаточно, нужно получить больше информации о работе скрипта, выполнить более тонкое профилирование работы или разобраться с зависаниями, обратитесь к таким системным инструментам как strace или в очень специфичной ситуации gdb (надеюсь с вами этого не произойдет)

Пример запуска отладки скрипта при помощи strace:

strace -C -f bash -x ./test.sh

Благодарю за ваше время и внимание, эффективного bash скриптинга вам!

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




Комментарии (6):