Разрабатывайте скрипты с опциями set -eu
- set -e - падать на ошибках
- set -u - считать ошибкой обращение к неопределенной переменной
- максимально быстрое выявление скрытых ошибок в коде.
- не надо самому писать многие проверки значений.
- Это сделает shell-код похожим на нормальный язык программирования, а не на набор последовательно выполняющихся команд, которым на всё пофиг.
«Ложные» и ложные падения. Про них расскажу подробнее. Вот пример скрипта:
#!/usr/bin/env bash
set -eu
x=1
((x++))
echo $x
x=0
((x++))
echo $x
Казалось бы, он должен вывести:
2
1
Но в bash версии 4+ увеличение переменной до единицы даё код возврата 1. Вроде даже находил почему и даже подумал что «ок, логично». Ещё часто «ложным» падением заканчивается возврат
cmd | grep "something"
при обработке текста, если этого something в stdin не было. В итоге код с set -e иногда обрастает конструкциями вроде
cmd | grep "something" || true
Суть set -e в том, чтобы все ошибки были обработаны. Go style прямо.
Несмотря на всю уродливость оного - такой код всё равно остаётся чище и проще, чем с ручной обработкой всех возможных ошибок. Благодаря set -u код обрастает «костылями» немного другого рода:
variable="${1:-default_value}"
Зато код проверки переданных функциям параметров становится значительно чище (практически пропадает).
На мой взгляд - да. Developer вариант должен быть максимально близким к продакшну.
@dshevchenko_biz подкинул ссылку на unofficial bash strict mode, в принципе он включает в себя совет по set -eu и ещё несколько важных вещей: http://redsymbol.net/articles/unofficial-bash-strict-mode/
Если вы используете bash в качестве языка программирования, то и относитесь к нему как к языку и используйте статический анализатор
Самый популярный анализатор для shell/bash на текущий момент - shellcheck. Есть плагины к Atom, можно запускать из консольки. Сейчас проект активно развивается, так что рекомендую следить за тем, насколько старую версию вы используете. К примеру за время которое я его использую (около полугода) пофиксили около трёх досадных багов, которые ложно обвиняли меня в плохом коде. Одного факта поиска неиспользуемых переменных уже достаточно, но он гораздо умнее, находит кучу подводных камней и must-have для джуниоров.
С помощью вот такого простого скрипта можно определять первичные цели для рефакторинга репозитория с bash-скриптами.
Вообще корень многих зол в bash - передача аргументов. Всегда вот все с ней тупят, мучаются и велосипедят. 190% людей путают
$*
$@
"$*"
"$@"
- 100% - это до того, как узнали чем они отличаются.
- 90% - после.
shellcheck может предупредить вас о ситуации:
rm -rf $DIRECTORY/$file
которая, если не использовать set -eu может превратится в
rm -rf /
в принципе современный coreutils итак не даст вам этого сделать, но ведь не только в / аргумент может схлопнуться.
Но в принципе писать код можно и без shellcheck, следуя двум правилам:
- кавычьте все "$переменные" в 99.9% случаев
- тестируйте упорнее
Bash точно такой же язык программирования и к нему действуют те же правила разработки, что и на других языках - никто не любит много кода.
Если в большинстве языков программирования эта проблема решается библиотеками, в bash лучше оформлять свой код в виде утилит. Принципиальной разницы между вызовом скрипта (читай команды) и функции нет.
Можно применять архитектурный подход, используемый в git - в зависимости от первого параметра ($1) запускать скрипт расположенный в, "$0-$1" которому прозрачно пробрасывать все параметры кроме первого. Дерево команд гита довольно здоровое, около 160 утилит, сам же git является маааленьким бинариком.
Конечно, никто не запрещает использовать конструкцию
source lib/yourlib.sh
source lib/yourlib2.sh
можно также писать
. lib/yourlib.sh
Но тут к нам всплывает один неприятный момент bash - переменные по умолчанию имеют глобальную область видимости. Sub-утилиту же гораздо легче подвернуть функциональному тестированию, при необходимости, да и просто запустить при разработке. Да и в коде это будет выглядеть понятнее. Сравните:
source ./mylib.sh
…
rate_my_code .
…
или
…
./rate_my_code.sh .
…
Во втором случае недоумения «откуда этот rate_my_code взялся?» у читателя не возникает. Также у rate_my_code.sh может быть свой --help, объясняющий что и как.
В питоне этот момент хорошо сглаживается прекрасным механизмом импортов, где всё явно:
from lib.sublib import func
сразу видно откуда берётся func.
Вы можете переопределять переменные, функции итд, подключая другие скрипты как библиотеки в нужный момент. Это может показать здоровским, но иногда может привести к тому, что необходимость обеспечивать обратную совместимость с кодом клиентов (если у проекта агрессивное автоматическое обновление) приведёт к появлению 700+строчных монстров, которые хочется отрефакторить и разбить на отдельные модули, но нельзя.
В конструкции
source libname.sh
есть возможность передачи аргументов. А аргументы можно анализировать. Благодаря этому можно получить что-то похожее на конструкцию:
if __name__ == '__main__' :
в python:
func1() {
echo qwe
}
# в конце файла
main() {
cmd1
cmd2
if somelogic "$@"; then
onemorecmd
fi
}
if [ "${1:-}" != ':' ]; then
main "$@"
fi
В результате можем получить в интерактивном шелле доступ ко всем функциям, определённым в скрипте:
$ source libname.sh :
$ func1
qwe
что даёт бонус в виде удобства тестирования и отладки.
Правило про "лучший код - тот, который не был написан" действует и в bash, с заменой "код" на "команда".