Как стать автором
Обновить

Лучшие практики bash-скриптов: краткое руководство по надежным и производительным скриптам bash

Время на прочтение6 мин
Количество просмотров35K
Всего голосов 58: ↑56 и ↓2+54
Комментарии70

Комментарии 70

Хорошая, годная статья. Но одна из лучших практик написания на bash — минимизировать использование bash.
Думаю это крайность, я бы переиначил так: Нужно иметь в голове для скриптов верхнюю границу их сложности, после которой лучше использовать что-нибудь другое.
В том то и проблема, что граница эта очень условная. Ибо, как показывает практика, отстрелить себе ногу или удалить /usr можно легко и непринужденно одной очевидной на первый взгляд строчкой, при этом, чтобы нормально покрыть ее защитной логикой требуется написать кучу вырвиглазой обвязки.

Честно говоря, не представляю сценария, где бы я мог удалить /usr.


Работать только в своём каталоге уже не модно?

Модно, но иногда люди делают sudo make install и аналогичные вещи. Был уже один такой баг, не помню в каком, но популярном пакете.


Впрочем, случайно удалить свой домашний каталог тоже то ещё удовольствие.

Эти проблемы появляются тогда, когда много пишешь на разных php/js/java/gо и др. А потом приходишь в bash и думаешь, я ж сеньор девелопер, что я тут в баше за 2 минуты не разберусь?

А если копнуть, то в bash и синтаксиса и возможностей и всего остального ненамного меньше, чем в любом другом скриптовом языке, и недооценивать его нельзя. Я вот буквально пару лет назад осмелился считать себя в баше сеньором, и то — всегда нахожу подводные пещеры, где я никогда не бывал.
Я знаю много опытных NIX админов, кто-то из них знает php, кто-то python, кто-то perl. Но объединяет их одно — все знают bash.

Или думают, что знают :-)

Если хорошо знаешь bash, то наоборот — лучше использовать его, чем кучу других решений.

Основная задача баш, в отличие от других языков программирования — работа с ОС и другими программами.
А где про «производительные скрипты» написано?
отлаживать = корень «лажать»
производить отладку
/зануда-off
Уровень гайда — КО.
Хотя, наверное для девляпса это божественное откровение
Что заставляет вас ходить в гайды и рассказывать всем что вы это и так знали?
По Вашему мнения, я не могу высказать свое оценочное суждение о представленном материяле?
Вы конечно можете, но более чем заслужено огребёте минуса т.к ваш комментарий не несёт никакого конструктива и никакой пользы.

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

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

Забыли про главную практику написания shell-скриптов, а именно не использовать bash-специфичный синтаксис и пользоваться POSIX shell.

Зачем вообще нужен bash если его фичами нельзя пользоваться? Не говоря уже о том что сейчас сложно найти систему где его нет (разве что MCU), да и статья не о shell-scripts а конкретно о bash.


Все эти заявления на тему "непортабельно" изрядно устарели — если им следовать, то можно вообще прекратить разработку чего-то нового, ибо "не рекомендуется", но я подозреваю что реально совместимость с 99% систем нужна едва-ли в 1% случаев.


Что касается POSIX… то он тоже не без нареканий, если всё притянуть за уши к соответствию, весь сделанный прогресс можно откатить лет на 20 назад, ибо работать в чём-то что строго следует стандарту (и писать под это код) — примерно как бегать с будкой на голове и обмотав ноги цепью.

BusyBox. Он включает некоторое количество «фишек» из Bash, но далеко не все. Так что меру знать в их использовании приходится.

А где он, кроме ембеда, встречается? В контейнерах? Ну там и баш немудрено поставить.

Помимо того в busybox сами по себе утилиты урезанные, поэтому даже если шелл-скрипт будет POSIX-совместимый, не факт что он будет нормально работать в таком кастрированном окружении, где все аналоги ГНУшных утилит имеют только 3-5 основных опций.
Вы так говорите «кроме ембеда», будто ембед можно просто отбросить

А в ембеде нужны переносимые скрипты?

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

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

Ну, хотя бы, потому, что полно UNIX дистрибутивов, где bash нет в составе базовой системы.

Насчёт UNIX ещё могу поверить, но кому сейчас нужно что-то, кроме Linux? Не-Linux — это маргинальный случай, для которого нужен отдельный набор инструментов, включая скрипты. Поэтому "полно UNIX дистрибутивов" — это явное преувеличение.


А если вы имели в виду Linux-дистрибутивы — было бы интересно узнать, в каких же из них нет bash. Раз уж их "полно", может, назовёте, хотя бы, 3-4?

Разумеется, не Linux. Все *BSD, например или линейка Solaris и её наследников.
Насчёт Linux не скажу, поскольку мне не слишком интересно ковыряться в этой куче.
Ну и, про Busybox вам уже написали.

Ок, сам Solaris можно сразу вычеркнуть:


GNU Bourne-Again Shell (bash) (/usr/bin/bash)
Bash is the default shell for users in Oracle Solaris.

Наследники уже тоже все почили.


Про *BSD я совсем забыл. Ок, самый популярный из них — Darwin, т.е. macOS, шёл с bash, в последнем релизе перешли на zsh.


Ок, остаются FreeBSD, OpenBSD, NetBSD (ну и DragonFlyBSD) с дефолтными /bin/sh и /bin/tcsh. Ну да, отличные системы, очень их уважаю и ценю, только вот что за скрипты должны быть переносимы между ними и Linux?

Даже если на посикс-шелле писать, они всё равно не будут переносимыми, потому что опции бздяшных стандартных утилит могут отличаться от их гнушных (надо сказать превосходящих) аналогов.
Напишите статью про posix shell, и где конкретно лучше писать именно так.

В статье не указано ни про портабельный софт ни про линукс шелл. Написано прямо про bash.

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

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

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

Поэтому писать на POSIX shell нужно не конкретно примерно всегда, а тогда когда к этому вынуждает рабочая обстановка.

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

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

К сожалению, конкретики не будет в связи с NDA. Скажу лишь, что компании не сказать чтобы крупные в мировых масштабах, но в своей сфере и в своём регионе заметные.
Базовая система там одна из мутаций Linux, однако никакой нужды использовать конкретно особенности bash не было вовсе. Широкое использование shell скриптинга связанно с чрезвычайно гетерогенной в силу исторических причин средой, как средство её унификации и сокращения инструментария используемого в backend, а также обеспечения портабельности за переделы экосистемы Linux.

К сожалению, конкретики не будет в связи с NDA

Ох-ох.
Совершенно не проблема сказать название области, не называя компанию, чтобы было понятно почему там такой зоопарк разных *nix систем.

Тем не менее вы подтверждаете, что «Писать на POSIX shell надо конкретно примерно всегда» это ваше личное IMHO базирующееся на вашем личном опыте работы, а не всемирная бест практика.
Совершенно не проблема сказать название области, не называя компанию, чтобы было понятно почему там такой зоопарк разных *nix систем.

Связь, если говорить в общем. Компания старая.

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

А, ну и насчёт портабельности: на работе везде RHEL-подобные системы и никаких эзотерических новых дистров не ожидается и не приветствуется. Зачем мне там портабельность? Для понтов?

Лимит на длину строки 140 символов, количество непустых строк не более 600. Вот и все правила, если в них баш не укладывается, пишем на питон/ансибл.

О да… Расскажите про это автору osync — 6503 строчки на bash, даже если убрать комментарии и пустые (их там мало), то всё равно наберется около 6000.

Ну кто-то захотел сделать велосипед, это полезно для практики (lsync может выполнять всю ту же работу и написан на си). В easyrsa 1700 строк. Но это же не значит, что так надо делать? У нас вот те правила, что я выше описал.

rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"

Зачем такой ужас? Есть же mktemp, который делает ровно то что нужно.

Полностью солидарен.
mktemp решает все эти вопросы легко и элегантно.
Более того, рандом имеет ненулевую вероятность наткнуться на существующий каталог, а mktemp гарантирует, что этого не произойдёт.

А как же правило "не изобретай велосипед"?


Как минимум по двум пунктам:


  1. lock фалы использовать через flock
  2. временные директории создавать через mktemp -d
Лучше вместо лок файлов использовать PID файлы

Вы имели ввиду использовать flock но не на отдельных файлах на PID файлах?
Тогда согласен.

Но есть нюанс: данный олдскульный хак (как результат, всего один fd у демонизированного процесса, если ему других файлов не надо) при запуске посредством unit-файла в экосистеме одного известного not-a-bug'а приводит к ругани в syslogjournald.
Судя по всему, происходит так:
  1. Вызываем flock() на будущий pid-файл. Понятно что сначала блокировка, может уже́ инстанс был запущен. Он создаётся.
  2. В systemd прилетает inotify, он бросается читать pid (а его ещё нету).
  3. Ругань про невозможность прочитать pid (а тем временем процесс отфоркался, написал в pid что получилось и на следующем заходе всё хорошо).

А почему не подходит Type=simple безо всяких PID-файлов?

Потому что демон?
Понимаю, сейчас это не модно. Но бывают случаи, когда нужно как можно скромнее потреблять ресурсы. Для этого когда-то был придуман двухкратный форк самого себя. С избавлением от всего лишнего, включая tty*.
Модель «simple» предполагает что «каким родился — таким и пригодился», а тут вообще всё меняется (кроме ppid).
  • Для подобных штук придумали ещё фокус с сигнализацией (STOP самому себе и CONT от поймавшего сигнал супервизора). Но как-то он популярности не получил. Судя по всему, архитектор systemd про это вообще не знал и сделал свой велосипед через dbus.
Но бывают случаи, когда нужно как можно скромнее потреблять ресурсы. Для этого когда-то был придуман двухкратный форк самого себя. С избавлением от всего лишнего, включая tty*.

Звучит как экономия на спичках, неужели это всё ещё востребовано, особенно, там, где есть systemd?

Отсутствие мыслей об экономии обычно заканчивается пофигизмом, несоблюдением элементарных стандартов и докером в продакшене.
Для обработки таких сценариев важно использовать встроенные функции set, такие как set -o errexit, set -o pipefail или set -o nounset в начале скрипта.

Это must have в любом скрипте, ещё удобно использовать сокращённый вариант: set -euo pipefail.


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

Ещё для самой базовой проверки синтаксиса можно использовать bash -n:


$ echo 'if then else fi' > badscript.sh
$ bash -n badscript.sh 
badscript.sh: line 1: syntax error near unexpected token `then'
badscript.sh: line 1: `if then else fi'
[2]$ 

Как и в других языках программирования высокого уровня, я всегда использую в моих скриптах bash собственные функции логирования, такие как __msg_info, __msg_error и так далее.

Я обычно делаю проще:


info() { >&2 echo -e  " INFO: $*"; }
die() { >&2 echo -e "ERROR: $*"; exit 1; }

Тогда всякие вспомогательные проверочки можно писать в лаконичном декларативном стиле, чем-то напоминающим Perl:


#/bin/bash

set -euo pipefail
info() { >&2 echo -e  " INFO: $*"; }
die() { >&2 echo -e "ERROR: $*"; exit 1; }

info "Checking the environment variables"
[[ -n ${MY_DIR:-} ]] \
  || die "'MY_DIR' is not defined"

[[ -d $MY_DIR ]] || mkdir -p $MY_DIR \
  || die "Could not create the directory '$MY_DIR'"

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


rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"

Тут нет проверки что такой каталог уже не используется из другого скрипта что может закончится плачевно.
Есть mktemp который занимается этим (может создать временный каталог\файл)

Ещё я читал, что есть такая переменная $RANDOM.

Есть. Не портабельно, к сожалению. Мнения насчёт нужно ли соблюдать POSIX sh или забить и писать под bash выше разошлись, но лично я стараюсь писать портабельно. Альтернатива, например, такая:


random="$(od -vAn -N2 -td2 </dev/urandom | tr -cd 0-9)" # 0-32767
Тут нет проверки что такой каталог уже не используется из другого скрипта что может закончится плачевно.
так mktemp тоже не проверяет, что каталог используется, но с учетом самого имени
$ mktemp
/var/folders/gn/qxngd93x399fjdmtzkqwbvnw0000gn/T/tmp.a0e7TEP5

думаю, что вероятность совпадения стремится к 0

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


Плюс, гарантируется что если он завершился успешно, то файл (или директорий) был создан с этим именем (именно создан — т.е. его там не было раньше) — а если сначала генерить имя, а потом "вручную" пытаться создать его — это race condition, пусть и с очень низкой вероятностью (хотя как знать, что там в random, может он глючной).


Но основная суть в том что mktemp проще чем приведённая в статье конструкция, выполняющая ту же функцию.

НЛО прилетело и опубликовало эту надпись здесь

Тоже хотел оставить эту ссылку, а именно цитату:


Shell should only be used for small utilities or simple wrapper scripts.

и


If you are writing a script that is more than 100 lines long, or that uses non-straightforward control flow logic, you should rewrite it in a more structured language now. Bear in mind that scripts grow. Rewrite your script early to avoid a more time-consuming rewrite at a later date.

ЗЫ: статья в целом хорошая и полезная, но этой цитаты не хватает :)

Полезно, учту.
Вроде как надо использовать lock-директории, а не lock-файлы. Т.к. mkdir вернет ошибку, если директория уже создана. С файлами такого сделать не получится.

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


Defensive programming в чистом виде. #pragma warn, любая опечатка может и будет использована для UB и т.д. Garbage in, garbage out.


Проблема с башом не в отдельных скриптах, а в том, что башизмы — это типовой метод склеивания разных кусков CI'я. Начинается невинно — список команд для выполнения. Потом появляется первый if или ||, потом кто-то добавляет строковые операции или полагается на специальную магию, а потом получившийся тонкий слой баша поверх всех остальных абстракций уже читать невозможно.


Чем меньше баша, тем лучше. И под башом я подразумеваю любой шелл sh-типа.

Тем не менее, иногда бывает полезно (и приятно) заменить страницу Ansible-плейбука на shell: >- из нескольких строчек...

В том месте, где вы используете баш "снизу" он наименее опасен. Потому что как только кто-то перестаёт понимать написанное или оно вызывает вопросы, то shell превращается в кастомный модуль с тестами (как минимум юнит, как максимум — интерграционными с коллекцией на galaxy).


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


Для меня code smell проекта — это функция экспоненты от цикломатической сложности баша (включая неявные условия от математики над переменными). 1 — ок, 2 — уже попахивает, 3 — конкретный code smell, 4 — беда-беда, 5 — я такое не читаю и от review такого отказываюсь.


Это не отменяет возможности писать на баше нормально. Мы, например, используем git vendor, который внутри — 230 строк баша, но написанных так хорошо, что напоминают нормальную программу.


Но как смазочный материал — опасно. Часто нужно, но чем меньше, тем лучше.

BTW, про тестирование в ansible не хотите статью написать?

Давно читал что-то вроде "Если бы все знали bash, sed и awk — то 90 % программ не понадобилось бы" Преувеличение, конечно. Но если добавить vim, screen и ssh то вроде и правда :-)). Глядя на wallix bastion отчетливо понимаешь "Победа сил добра над силами разума".

Просто пусть полежит здесь.
Некогда для своего же удобства был написан мини пошаговый db для bash. Просто красивый вывод исполняемой строки с глубиной вложения и с возможностью продолжения по нажатию клавиши.

debug_handler()
{
    local dkey="$1"
    echo "[DEBUG[]]"
}

PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]-main}():[${SHLVL},${BASH_SUBSHELL},e=$?]`[ $DEBUG_BY_LINE ] && read -s -N 1 -r DBGKEY && debug_handler "$DBGKEY"`# '


могло включаться в любом месте файла, который надо отладить просто добавлением DEBUG_BY_LINE=1
source ./debug
# .....
DEBUG_BY_LINE=1
# ... то что нужно отладить
DEBUG_BY_LINE=0
Как по мне — то это пример write-only кода. То есть такого кода, который можно написать, но прочитать (и понять) уже практически никто не может, зачастую даже и сам автор.
Да не, тут все понятно, но я бы сказал что слишком оверхед для дебага.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий