Баш-скрипты
Программист должен помнить не только о вещах, которые нужно делать. Обратное тоже важно: запоминайте, чего делать не нужно. Плохой опыт — тоже опыт. Он поможет не упасть там, где спотыкаются другие.
Одна из таких вещей — скрипты на баше. Я взял за правило: никаких баш-скриптов. Когда хочется написать на нем что-то длиннее пяти строк, закрывайте ноут и идите за молоком. Потом вернитесь и сделайте правильно — то есть без баша.
Шелл-скрипты хрупки и тяжелы в поддержке. Их трудно читать и отлаживать. Они опираются на сторонние утилиты, которые где-то есть, а где-то нет. Поведение зависит от платформы и флагов.
Шелл — это плавное погружение в смоляную яму, из которой не выбраться. Год-другой — и скрипт занимает экраны, никто не знает, как он работает. Лучше не трогать.
Замечу, что сказанное не относится к случаям, когда вызывают несколько утилит, соединяя их каналы, например:
> ps aux | grep java
Это совершенно нормально. Я имею в виду скрипты, где шелл выступает в роли настоящего языка программирования, то есть:
- активно используются переменные;
- циклы, ветвления, переходы;
- объявление и вызов функций;
- импорт других скриптов.
Все вместе это становится таким адом, что лучше уволиться, чем ворошить осиное гнездо (или, в английской идиоме — тараканье).
Кусок скрипта откуда-то из интернета:
no_more_args=1 ;
-*)
if [ "x$no_more_args" = "x1" ] ;
then
dirs[$num_dirs]="$1";
let "num_dirs++"
else
if [[ "x${1:1:1}" != "x-" && "x$1" =~ "x-.*a.*" ]] ;
then
no_dots=0;
fi
opts="$opts $1";
fi
*)
dirs[$num_dirs]="$1";
let "num_dirs++"
esac
shift
Для меня он выглядит как китайская грамота. Что такое [ "x$no_more_args" = "x1" ]
? Что такое [[ "x${1:1:1}" != "x-" && "x$1" =~ "x-.*a.*" ]]
? Как это читать и поддерживать?
Во времена Unix шелл-скрипты были прорывом, с этим никто не спорит. Однако время не стоит на месте, и созданы более удобные инструменты. Вот несколько примеров.
Если с проектом нужно делать много мелких вещей, подойдет make. Напомню, make — это набор именованных действий. Кроме вызова по отдельности их можно комбинировать, например собрать бек и фронт за раз. В любом проекте я завожу Makefile и помещаю туда все, что придет в голову, например:
- сборку проекта
- прогон тестов и миграций
- очистку временных файлов и папок
- запуск и остановку Докера
- много чего еще
Пример большого Make-файла можно посмотреть в репозитории с книгой.
Далее: если нужны действительно сложные скрипты, возьмите Питон. Даже на голом Питоне писать в разы легче, чем на баше. Для Питона созданы библиотеки с изящным API, чтобы вызывать процессы, двигать файлы и прочее.
Да, Питон требует окружения. Однако если у вас сложные скрипты, он окупается. Установку Питона и его барахла поместите в Makefile.
Для управления машинами лучше писать плейбуки на YAML. Кроме Ansible, полно других утилит для конфигураций.
Любые действия лучше задавать декларативно. Если вы пишете скрипты, скорей всего, вы не знаете о нужной утилите.
Еще одна интересная мысль — генерация шелла из другого языка. Подобно тому, как из TypeScript производят JavaScript, можно произвести шелл из условного Лиспа или Питона. Когда я работал в Exoscale, у нас была библиотека для генерации шелла на Кложе. Я ей не пользовался, но само наличие говорит о том, что в руководстве понимали риски ручного скриптописания.
Припомню только один случай, когда от шелла не отвертишься — это докер-контейнеры. Кроме голой операционки там ничего нет, поэтому сжимаешь булки и пишешь на шелле. Тянуть мегабайты Питона, конечно, будет плохим вариантом.
В общем, чем раньше вы замените шелл-скрипт на что-то профильное, тем лучше.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter
никита, 9th Feb 2023, link
не согласен.
полезно знать то, что ты написал
но не писать скрипты на баше, потому что их можно написать плохо - плохая причина
пиши хорошо: не делай кучу пайпов, используй функции, дели скрипты на части, называй переменные
баш это такой же язык программирования и всегда лучше любого питона с рантаймом и зависимостями для не сильно больших задач
Евгений, 10th Feb 2023, link
У Makefile тоже громоздкий эзотерический синтаксис родом из 70-х, в котором очень легко ошибиться. Как в Makefile экранируются и раскрываются переменные никто толком не знает. На работе только позавчера было затрачено некоторое количество времени на исправление вызванной этим ошибки. Команда make не принимает аргументы для целей, из-за этого тоже приходится городить костыли.
Евгений, 10th Feb 2023, link
Например, по идее, приведённый пример Makefile содержит ошибку из-за рекурсивного раскрытия переменных при использовании: https://stackoverflow.com/questions/448910/what-is-the-difference-between-the-gnu-makefile-variable-assignments-a
Но я попытался запустить пример со StackOverflow, и несколько подобных ему, и вызвать ошибку не получилось: переменные раскрылись только один раз.
Что, по-моему, только подтверждает тезис, что язык Makefile никто не понимает, все только предполагают, что понимают (такая же ситуация, кстати, части случается и с регулярными выражениями, но это отдельная тема).
Евгений, 10th Feb 2023, link
Ну и последний коммент: в приведенном в качестве примера Makefile не используется директива
.PHONY
. Значит, рано или поздно, возникнет очень сложно отлаживаемая ошибка, когда в репе появится файл с названием цели, и make молча перестанет отрабатывать команды.Ivan Grishaev, 10th Feb 2023, link
Евгений,
да, Make не идеален. Он тоже родом из 70-х и несет свои проблемы. Однако я счел его более удобным, чем голый shell.
не совсем понял, где ошибка. Этим файлом я пользовался, без преувеличения, тысячу раз, и проблем не возникало.
Насчет PHONY знаю, было лень их расставлять. Обычно делаю это только по необходимости.
Евгений, 11th Feb 2023, link
Ошибка теоретически в том, что переменные определяемые через
=
(а не:=
) должны раскрываться при каждом использовании. Однако у меня они так не работают. А перечитывать руководство по make лень. Но потенциально с ними всё же можно получить проблемы в будущем.Потенциально это тоже выстрел в ногу. Рано или поздно файл с таким именем может появиться, и тогда, как и с любым другим багом, придётся тратить ненулевое время на поиски.